From f3e8e902bcde96639925713be9d33f530aded415 Mon Sep 17 00:00:00 2001 From: Mark Samuel Date: Wed, 13 Aug 2025 09:25:27 +0300 Subject: [PATCH 1/2] [3.0.0] - 2025-08-13 ### Major Changes #### Added - **TypeScript Integration**: - Converted some core classes (Slide, WorkingFile) to TypeScript for better type safety - Added TypeScript configuration for future project-wide adoption - **Enhanced Text Customization**: - Font selection from system fonts (fixes #40) - Text positioning controls - Toggleable text background - **Video Controls**: - Mute/unmute functionality for videos - Improved video handling with black video fallback - **Project Management**: - New TAR-based project format replacing ZIP (fixes 2GB size limit) - Direct archive reading capability (fixes #32) #### Changed - **UI Improvements**: - Redesigned slide creator interface - Better slide thumbnail management - Improved sidebar navigation - **Architectural Updates**: - Refactored monolithic SlideCreator into modular components following SOLID principles - Implemented new MediaResponder class for better media handling - **Performance**: - Optimized video streaming and resource loading - Reduced memory usage for large projects #### Fixed - **Critical Issues**: - Resolved 2GB project size limitation - Fixed archive access problems - **Stability**: - Improved error handling during project save/load - Better resource cleanup when closing projects - **UX**: - Smoother transitions between slides - More reliable file operations ### Technical Details #### New Features - Added font selector with system font detection - Implemented text positioning with drag-and-drop - Created new video toolbar with mute control - Added project save confirmation before quit #### Code Improvements - TypeScript migration for core components - Modular architecture with separate classes for: - Canvas rendering - Sidebar management - Text editing - Video controls - Improved error handling and logging #### Dependency Updates - Updated Electron to latest stable version - Replaced JSZip with TAR for archive handling - Added new dev dependencies for TypeScript support --- CHANGELOG | 65 + extraResources/fontlist/LICENSE | 21 + extraResources/fontlist/README.md | 70 + extraResources/fontlist/demo.js | 15 + extraResources/fontlist/getSystemFonts.js | 21 + extraResources/fontlist/index.d.ts | 13 + extraResources/fontlist/index.js | 44 + extraResources/fontlist/libs/darwin/fontlist | Bin 0 -> 168560 bytes .../fontlist/libs/darwin/fontlist.m | 14 + extraResources/fontlist/libs/darwin/index.js | 68 + extraResources/fontlist/libs/linux/index.js | 28 + extraResources/fontlist/libs/standardize.js | 29 + .../fontlist/libs/win32/GetSystemFonts.ps1 | 61 + extraResources/fontlist/libs/win32/fonts.vbs | 29 + .../fontlist/libs/win32/getByPowerShell.js | 79 + .../fontlist/libs/win32/getByVBS.js | 79 + extraResources/fontlist/libs/win32/index.js | 34 + extraResources/fontlist/package.json | 20 + forge.config.js | 176 +- package-lock.json | 1426 ++++++++++++++++- package.json | 24 +- src/index.js | 474 +++--- src/renderer/ShowCreateView/index.html | 465 +++++- src/renderer/ShowCreateView/js/fileOpen.js | 100 +- ...24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + src/renderer/css/_style.scss | 29 +- .../js/Classes/CanvasRendererClass.js | 343 ++++ src/renderer/js/Classes/LyricManagerClass.js | 58 + src/renderer/js/Classes/ShowClass.js | 125 +- src/renderer/js/Classes/ShowCreator.js | 190 +++ src/renderer/js/Classes/ShowCreatorClass.js | 340 ---- .../js/Classes/ShowPresentationBaseClass.js | 411 ++--- .../js/Classes/ShowPresenterViewClass.js | 236 +-- .../js/Classes/SidebarRendererClass.js | 119 ++ src/renderer/js/Classes/Slide.ts | 188 +++ src/renderer/js/Classes/SlideClass.js | 182 --- src/renderer/js/Classes/SlideManager.js | 106 ++ src/renderer/js/Classes/TextEditorClass.js | 57 + src/renderer/js/Classes/Utils.js | 108 ++ src/renderer/js/Classes/VideoToolbarClass.js | 28 + src/renderer/openFileDialog/index.html | 7 +- src/renderer/openFileDialog/index.js | 46 +- src/renderer/preload.js | 45 +- src/renderer/presentationView/renderer.js | 2 +- src/renderer/presenterView/js/fileOpen.js | 2 +- src/utils/MediaResponderClass.js | 104 ++ src/workingFile.ts | 317 ++++ src/{workingFile.js => workingFileTemp.js} | 10 +- tsconfig.json | 15 + webpack.main.config.js | 24 +- webpack.renderer.config.js | 29 +- webpack.rules.js | 74 +- 52 files changed, 5028 insertions(+), 1523 deletions(-) create mode 100644 CHANGELOG create mode 100644 extraResources/fontlist/LICENSE create mode 100644 extraResources/fontlist/README.md create mode 100644 extraResources/fontlist/demo.js create mode 100644 extraResources/fontlist/getSystemFonts.js create mode 100644 extraResources/fontlist/index.d.ts create mode 100644 extraResources/fontlist/index.js create mode 100644 extraResources/fontlist/libs/darwin/fontlist create mode 100644 extraResources/fontlist/libs/darwin/fontlist.m create mode 100644 extraResources/fontlist/libs/darwin/index.js create mode 100644 extraResources/fontlist/libs/linux/index.js create mode 100644 extraResources/fontlist/libs/standardize.js create mode 100644 extraResources/fontlist/libs/win32/GetSystemFonts.ps1 create mode 100644 extraResources/fontlist/libs/win32/fonts.vbs create mode 100644 extraResources/fontlist/libs/win32/getByPowerShell.js create mode 100644 extraResources/fontlist/libs/win32/getByVBS.js create mode 100644 extraResources/fontlist/libs/win32/index.js create mode 100644 extraResources/fontlist/package.json create mode 100644 src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/renderer/js/Classes/CanvasRendererClass.js create mode 100644 src/renderer/js/Classes/LyricManagerClass.js create mode 100644 src/renderer/js/Classes/ShowCreator.js delete mode 100644 src/renderer/js/Classes/ShowCreatorClass.js create mode 100644 src/renderer/js/Classes/SidebarRendererClass.js create mode 100644 src/renderer/js/Classes/Slide.ts delete mode 100644 src/renderer/js/Classes/SlideClass.js create mode 100644 src/renderer/js/Classes/SlideManager.js create mode 100644 src/renderer/js/Classes/TextEditorClass.js create mode 100644 src/renderer/js/Classes/Utils.js create mode 100644 src/renderer/js/Classes/VideoToolbarClass.js create mode 100644 src/utils/MediaResponderClass.js create mode 100644 src/workingFile.ts rename src/{workingFile.js => workingFileTemp.js} (95%) create mode 100644 tsconfig.json diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..eb33db0 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,65 @@ +# Changelog + +## [3.0.0] - 2025-08-13 + +### Major Changes + +#### Added +- **TypeScript Integration**: + - Converted some core classes (Slide, WorkingFile) to TypeScript for better type safety + - Added TypeScript configuration for future project-wide adoption +- **Enhanced Text Customization**: + - Font selection from system fonts (fixes #40) + - Text positioning controls + - Toggleable text background +- **Video Controls**: + - Mute/unmute functionality for videos + - Improved video handling with black video fallback +- **Project Management**: + - New TAR-based project format replacing ZIP (fixes 2GB size limit) + - Direct archive reading capability (fixes #32) + +#### Changed +- **UI Improvements**: + - Redesigned slide creator interface + - Better slide thumbnail management + - Improved sidebar navigation +- **Architectural Updates**: + - Refactored monolithic SlideCreator into modular components following SOLID principles + - Implemented new MediaResponder class for better media handling +- **Performance**: + - Optimized video streaming and resource loading + - Reduced memory usage for large projects + +#### Fixed +- **Critical Issues**: + - Resolved 2GB project size limitation + - Fixed archive access problems +- **Stability**: + - Improved error handling during project save/load + - Better resource cleanup when closing projects +- **UX**: + - Smoother transitions between slides + - More reliable file operations + +### Technical Details + +#### New Features +- Added font selector with system font detection +- Implemented text positioning with drag-and-drop +- Created new video toolbar with mute control +- Added project save confirmation before quit + +#### Code Improvements +- TypeScript migration for core components +- Modular architecture with separate classes for: + - Canvas rendering + - Sidebar management + - Text editing + - Video controls +- Improved error handling and logging + +#### Dependency Updates +- Updated Electron to latest stable version +- Replaced JSZip with TAR for archive handling +- Added new dev dependencies for TypeScript support diff --git a/extraResources/fontlist/LICENSE b/extraResources/fontlist/LICENSE new file mode 100644 index 0000000..de813fd --- /dev/null +++ b/extraResources/fontlist/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 oldj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extraResources/fontlist/README.md b/extraResources/fontlist/README.md new file mode 100644 index 0000000..fda70c0 --- /dev/null +++ b/extraResources/fontlist/README.md @@ -0,0 +1,70 @@ +# font-list + +`font-list` is a Node.js package for listing the fonts available on your system. + +Current version supports **MacOS**, **Windows**, and **Linux**. + +## Install + +```bash +npm install font-list +``` + +## Usage + +```js +const fontList = require('font-list') + +fontList.getFonts() + .then(fonts => { + console.log(fonts) + }) + .catch(err => { + console.log(err) + }) +``` + +or like this in TypeScript: + +```ts +import { getFonts } from 'font-list' + +getFonts() + .then(fonts => { + console.log(fonts) + }) + .catch(err => { + console.log(err) + }) +``` + +The return value `fonts` is an Array, looks like: + +``` +[ '"Adobe Arabic"', + '"Adobe Caslon Pro"', + '"Adobe Devanagari"', + '"Adobe Fan Heiti Std"', + '"Adobe Fangsong Std"', + 'Arial', + ... + ] +``` + +If the font name contains spaces, the name will be wrapped in double quotes, otherwise there will be no double quotes, +for example: `'"Adobe Arabic"'`, `'Arial'`. + +If you don't want font names that contains spaces to be wrapped in double quotes, pass the options object +with `disableQuoting` set to true when calling the method `getFonts`: + +```js +const fontList = require('font-list') + +fontList.getFonts({ disableQuoting: true }) + .then(fonts => { + console.log(fonts) + }) + .catch(err => { + console.log(err) + }) +``` diff --git a/extraResources/fontlist/demo.js b/extraResources/fontlist/demo.js new file mode 100644 index 0000000..9435b1e --- /dev/null +++ b/extraResources/fontlist/demo.js @@ -0,0 +1,15 @@ +/** + * @author oldj + * @blog http://oldj.net + */ + +'use strict' + +require('./index').getFonts() + .then(fonts => { + //console.log(fonts) + console.log(fonts.join('\n')) + }) + .catch(err => { + console.log(err) + }) diff --git a/extraResources/fontlist/getSystemFonts.js b/extraResources/fontlist/getSystemFonts.js new file mode 100644 index 0000000..08f5625 --- /dev/null +++ b/extraResources/fontlist/getSystemFonts.js @@ -0,0 +1,21 @@ +const { getFonts } = require("./index"); + +function getSystemFonts() { + return new Promise((resolve, reject) => { + getFonts({ disableQuoting: true }) + .then((fonts) => { + fonts = [...new Set(fonts)]; + resolve(fonts || []); + }) + .catch((err) => { + resolve([]); + }); + }); +} + +(async () => { + const fonts = await getSystemFonts(); + // process 处理 + process.send(fonts); + // console.log('fonts', fonts); +})(); diff --git a/extraResources/fontlist/index.d.ts b/extraResources/fontlist/index.d.ts new file mode 100644 index 0000000..39ba5f8 --- /dev/null +++ b/extraResources/fontlist/index.d.ts @@ -0,0 +1,13 @@ +/** + * index.d.ts + * @author: oldj + * @homepage: https://oldj.net + */ + +interface IOptions { + disableQuoting: boolean; +} + +type FontList = string[] + +export function getFonts (options?: IOptions): Promise; diff --git a/extraResources/fontlist/index.js b/extraResources/fontlist/index.js new file mode 100644 index 0000000..3e3214f --- /dev/null +++ b/extraResources/fontlist/index.js @@ -0,0 +1,44 @@ +/** + * @author oldj + * @blog http://oldj.net + */ + +"use strict"; + +const standardize = require("./libs/standardize"); +const platform = process.platform; + +let getFontsFunc; +switch (platform) { + case "darwin": + getFontsFunc = require("./libs/darwin"); + break; + case "win32": + getFontsFunc = require("./libs/win32"); + break; + case "linux": + getFontsFunc = require("./libs/linux"); + break; + default: + throw new Error(`Error: font-list can not run on ${platform}.`); +} + +const defaultOptions = { + disableQuoting: false, +}; + +exports.getFonts = async (options) => { + options = Object.assign({}, defaultOptions, options); + + let fonts = await getFontsFunc(); + /* fonts = standardize(fonts, options) + + */ + fonts.sort((a, b) => { + return a.replace(/^['"]+/, "").toLocaleLowerCase() < + b.replace(/^['"]+/, "").toLocaleLowerCase() + ? -1 + : 1; + }); + return fonts; +}; diff --git a/extraResources/fontlist/libs/darwin/fontlist b/extraResources/fontlist/libs/darwin/fontlist new file mode 100644 index 0000000000000000000000000000000000000000..dcde35f505d5c393fab81f02d8d23a7546879f85 GIT binary patch literal 168560 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 +#import + +int main(int argc, const char * argv[]) { + @autoreleasepool { + NSFontManager *fontManager = [NSFontManager sharedFontManager]; + NSArray *fontFamilyNames = [[fontManager availableFontFamilies] sortedArrayUsingSelector:@selector(compare:)]; + + for (NSString *familyName in fontFamilyNames) { + printf("%s\n", [familyName UTF8String]); + } + } + return 0; +} diff --git a/extraResources/fontlist/libs/darwin/index.js b/extraResources/fontlist/libs/darwin/index.js new file mode 100644 index 0000000..0fba2ba --- /dev/null +++ b/extraResources/fontlist/libs/darwin/index.js @@ -0,0 +1,68 @@ +/** + * index + * @author oldj + * @blog https://oldj.net + */ + +'use strict' + +const path = require('path') +const execFile = require('child_process').execFile +const exec = require('child_process').exec +const util = require('util') + +const pexec = util.promisify(exec) + +const bin = path.join(__dirname, 'fontlist') +const font_exceptions = ['iconfont'] + +async function getBySystemProfiler () { + const cmd = `system_profiler SPFontsDataType | grep "Family:" | awk -F: '{print $2}' | sort | uniq` + const {stdout} = await pexec(cmd, {maxBuffer: 1024 * 1024 * 10}) + return stdout.split('\n').map(f => f.trim()).filter(f => !!f) +} + +async function getByExecFile () { + return new Promise(async (resolve, reject) => { + execFile(bin, {maxBuffer: 1024 * 1024 * 10}, (error, stdout, stderr) => { + if (error) { + reject(error) + return + } + + let fonts = [] + if (stdout) { + //fonts = fonts.concat(tryToGetFonts(stdout)) + fonts = fonts.concat(stdout.split('\n')) + } + if (stderr) { + //fonts = fonts.concat(tryToGetFonts(stderr)) + console.error(stderr) + } + + fonts = Array.from(new Set(fonts)) + .filter(i => i && !font_exceptions.includes(i)) + + resolve(fonts) + }) + }) +} + +module.exports = async () => { + let fonts = [] + try { + fonts = await getByExecFile() + } catch (e) { + console.error(e) + } + + if (fonts.length === 0) { + try { + fonts = await getBySystemProfiler() + } catch (e) { + console.error(e) + } + } + + return fonts +} diff --git a/extraResources/fontlist/libs/linux/index.js b/extraResources/fontlist/libs/linux/index.js new file mode 100644 index 0000000..f8bea32 --- /dev/null +++ b/extraResources/fontlist/libs/linux/index.js @@ -0,0 +1,28 @@ +/** + * index + * @author: oldj + * @homepage: https://oldj.net + */ + +const exec = require('child_process').exec +const util = require('util') + +const pexec = util.promisify(exec) + +async function binaryExists(binary) { + const { stdout } = await pexec(`whereis ${binary}`) + return stdout.length > (binary.length + 2) +} + +module.exports = async () => { + const fcListBinary = await binaryExists('fc-list') + ? 'fc-list' + : 'fc-list2' + + const cmd = fcListBinary + ' -f "%{family[0]}\\n"' + + const { stdout } = await pexec(cmd, { maxBuffer: 1024 * 1024 * 10 }) + const fonts = stdout.split('\n').filter(f => !!f) + + return Array.from(new Set(fonts)) +} diff --git a/extraResources/fontlist/libs/standardize.js b/extraResources/fontlist/libs/standardize.js new file mode 100644 index 0000000..e202b95 --- /dev/null +++ b/extraResources/fontlist/libs/standardize.js @@ -0,0 +1,29 @@ +/** + * @author oldj + * @blog http://oldj.net + */ + +'use strict' + +module.exports = function (fonts, options) { + fonts = fonts.map(i => { + // parse unicode names, eg: '"\\U559c\\U9e4a\\U805a\\U73cd\\U4f53"' -> '"喜鹊聚珍体"' + try { + i = i.replace(/\\u([\da-f]{4})/ig, (m, s) => String.fromCharCode(parseInt(s, 16))) + } catch (e) { + console.log(e) + } + + if (options && options.disableQuoting) { + if (i.startsWith('"') && i.endsWith('"')) { + i = `${i.substr(1, i.length - 2)}` + } + } else if (i.match(/[\s()+]/) && !i.startsWith('"')) { + i = `"${i}"` + } + + return i + }) + + return fonts +} diff --git a/extraResources/fontlist/libs/win32/GetSystemFonts.ps1 b/extraResources/fontlist/libs/win32/GetSystemFonts.ps1 new file mode 100644 index 0000000..afba40a --- /dev/null +++ b/extraResources/fontlist/libs/win32/GetSystemFonts.ps1 @@ -0,0 +1,61 @@ +# GetSystemFonts.ps1 +# Lists all system fonts with their localized names, available languages, and styles in JSON + +# Ensure UTF-8 output +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Add-Type -AssemblyName PresentationCore + +$fams = [Windows.Media.Fonts]::SystemFontFamilies +$result = @() + +$languageSamples = @{ + "en" = 0x0041 # Latin capital A + "ar" = 0x0627 # Arabic Alef +} +foreach ($fam in $fams) { + # Preferred family name (try zh-CN first, fallback to en-US) + $name = '' + if (-not $fam.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$name)) { + $name = $fam.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + + + # Supported language codes + $langs = @() + + foreach ($typeface in $fam.GetTypefaces()) { + try { + $glyph = New-Object Windows.Media.GlyphTypeface $typeface.FontUri + foreach ($lang in $languageSamples.Keys) { + if ($glyph.CharacterToGlyphMap.ContainsKey($languageSamples[$lang])) { + $langs += $lang + } + } + } + catch {} + } + + $langs = $langs | Sort-Object -Unique + + + # All typefaces (weights/styles) + $faces = @() + foreach ($tf in $fam.GetTypefaces()) { + $face = '' + if (-not $tf.FaceNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$face)) { + $face = $tf.FaceNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + if ([string]::IsNullOrWhiteSpace($face)) { + $face = "$($tf.Weight) $($tf.Style)" + } + $faces += $face + } + + $result += [PSCustomObject]@{ + name = $name + # faces = $faces + } +} + +$result | ConvertTo-Json -Depth 4 diff --git a/extraResources/fontlist/libs/win32/fonts.vbs b/extraResources/fontlist/libs/win32/fonts.vbs new file mode 100644 index 0000000..3126578 --- /dev/null +++ b/extraResources/fontlist/libs/win32/fonts.vbs @@ -0,0 +1,29 @@ +Option Explicit + +Dim objShell, objFSO, objFile, objFolder +Dim objFolderItem, colItems, objFont +Dim strFileName + + +Const FONTS = &H14& ' Fonts Folder + +' Instantiate Objects +Set objShell = CreateObject("Shell.Application") +Set objFolder = objShell.Namespace(FONTS) +Set objFolderItem = objFolder.Self +Set colItems = objFolder.Items +Set objFSO = CreateObject("Scripting.FileSystemObject") + +For Each objFont in colItems + WScript.StdOut.WriteLine(objFont.Path & vbtab & objFont.Name) +Next + +Set objShell = nothing +Set objFile = nothing +Set objFolder = nothing +Set objFolderItem = nothing +Set colItems = nothing +Set objFont = nothing +Set objFSO = nothing + +wscript.quit diff --git a/extraResources/fontlist/libs/win32/getByPowerShell.js b/extraResources/fontlist/libs/win32/getByPowerShell.js new file mode 100644 index 0000000..62c81c5 --- /dev/null +++ b/extraResources/fontlist/libs/win32/getByPowerShell.js @@ -0,0 +1,79 @@ +/** + * getByPowerShell + * @author: oldj + * @homepage: https://oldj.net + */ + +const exec = require("child_process").exec; + +const parse = (str) => { + return str + .split("\n") + .map((ln) => ln.trim()) + .filter((f) => !!f); +}; +/* +@see https://superuser.com/questions/760627/how-to-list-installed-font-families + + chcp 65001 | Out-Null + Add-Type -AssemblyName PresentationCore + $families = [Windows.Media.Fonts]::SystemFontFamilies + foreach ($family in $families) { + $name = '' + if (!$family.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$name)) { + $name = $family.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + echo $name + } +*/ +module.exports = () => + new Promise((resolve, reject) => { + let cmd = `chcp 65001|powershell -command "chcp 65001|Out-Null;Add-Type -AssemblyName PresentationCore;$families=[Windows.Media.Fonts]::SystemFontFamilies;foreach($family in $families){$name='';if(!$family.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'),[ref]$name)){$name=$family.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')]}echo $name}"`; + /* + cmd = `chcp 65001 | powershell -command "chcp 65001 | Out-Null; +Add-Type -AssemblyName PresentationCore; +$fams = [Windows.Media.Fonts]::SystemFontFamilies; +$result = @(); +foreach ($fam in $fams) { + # Pick preferred name + $name = ''; + if (-not $fam.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$name)) { + $name = $fam.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + + # Gather languages this font supports + $langs = $fam.FamilyNames.Keys | ForEach-Object { $_.IetfLanguageTag } + + # Gather all typefaces + $faces = @(); + foreach ($tf in $fam.GetTypefaces()) { + $face = ''; + if (-not $tf.FaceNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$face)) { + $face = $tf.FaceNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + if ([string]::IsNullOrWhiteSpace($face)) { + $face = "$($tf.Weight) $($tf.Style)" + } + $faces += $face + } + + $result += [PSCustomObject]@{ + name = $name + languages = $langs + faces = $faces + } +} +$result | ConvertTo-Json -Depth 4 -Encoding UTF8 +"`; */ + // const scriptPath = path.join(__dirname, "GetSystemFonts.ps1"); + + // execFile("powershell", ["-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8" },(err, stdout) => { + exec(cmd, { maxBuffer: 1024 * 1024 * 10 }, (err, stdout) => { + if (err) { + reject(err); + return; + } + + resolve(parse(stdout)); + }); + }); diff --git a/extraResources/fontlist/libs/win32/getByVBS.js b/extraResources/fontlist/libs/win32/getByVBS.js new file mode 100644 index 0000000..af12faa --- /dev/null +++ b/extraResources/fontlist/libs/win32/getByVBS.js @@ -0,0 +1,79 @@ +/** + * getByVBS + * @author: oldj + * @homepage: https://oldj.net + */ + +const os = require('os') +const fs = require('fs') +const path = require('path') +const execFile = require('child_process').execFile +const util = require('util') + +const p_copyFile = util.promisify(fs.copyFile) + +function tryToGetFonts(s) { + let a = s.split('\n') + if (a[0].includes('Microsoft')) { + a.splice(0, 3) + } + + a = a.map(i => { + i = i + .split('\t')[0] + .split(path.sep) + i = i[i.length - 1] + + if (!i.match(/^[\w\s]+$/)) { + i = '' + } + + i = i + .replace(/^\s+|\s+$/g, '') + .replace(/(Regular|常规)$/i, '') + .replace(/^\s+|\s+$/g, '') + + return i + }) + + return a.filter(i => i) +} + +async function writeToTmpDir(fn) { + let tmp_fn = path.join(os.tmpdir(), 'node-font-list-fonts.vbs') + await p_copyFile(fn, tmp_fn) + return tmp_fn +} + +module.exports = async () => { + let fn = path.join(__dirname, 'fonts.vbs') + + const is_in_asar = fn.includes('app.asar') + if (is_in_asar) { + fn = await writeToTmpDir(fn) + } + + return new Promise((resolve, reject) => { + let cmd = `cscript` + + execFile(cmd, [fn], { maxBuffer: 1024 * 1024 * 10 }, (err, stdout, stderr) => { + let fonts = [] + + if (err) { + reject(err) + return + } + + if (stdout) { + //require('electron').dialog.showMessageBox({message: 'stdout: ' + stdout}) + fonts = fonts.concat(tryToGetFonts(stdout)) + } + if (stderr) { + //require('electron').dialog.showMessageBox({message: 'stderr: ' + stderr}) + fonts = fonts.concat(tryToGetFonts(stderr)) + } + + resolve(fonts) + }) + }) +} diff --git a/extraResources/fontlist/libs/win32/index.js b/extraResources/fontlist/libs/win32/index.js new file mode 100644 index 0000000..f9d2dc2 --- /dev/null +++ b/extraResources/fontlist/libs/win32/index.js @@ -0,0 +1,34 @@ +/** + * index + * @author oldj + * @blog https://oldj.net + */ + +"use strict"; + +const os = require("os"); +const getByPowerShell = require("./getByPowerShell"); +const getByVBS = require("./getByVBS"); + +const methods_new = [getByPowerShell, getByVBS]; +const methods_old = [getByVBS, getByPowerShell]; + +module.exports = async () => { + let fonts = []; + + // @see {@link https://stackoverflow.com/questions/42524606/how-to-get-windows-version-using-node-js} + let os_v = parseInt(os.release()); + let methods = os_v >= 10 ? methods_new : methods_old; + console.log(os_v >= 10); + + for (let method of methods) { + try { + fonts = await method(); + if (fonts.length > 0) break; + } catch (e) { + console.log(e); + } + } + + return fonts; +}; diff --git a/extraResources/fontlist/package.json b/extraResources/fontlist/package.json new file mode 100644 index 0000000..3f84178 --- /dev/null +++ b/extraResources/fontlist/package.json @@ -0,0 +1,20 @@ +{ + "name": "font-list", + "version": "1.5.1", + "description": "list system fonts", + "main": "index.js", + "scripts": { + "demo": "node demo.js", + "test": "echo \"Error: no test specified\" && exit 1", + "p": "npm publish --access public" + }, + "keywords": [ + "font" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/oldj/node-font-list" + }, + "author": "oldj", + "license": "MIT" +} diff --git a/forge.config.js b/forge.config.js index 88b518a..23ae61d 100644 --- a/forge.config.js +++ b/forge.config.js @@ -1,87 +1,99 @@ + module.exports = { - packagerConfig: { - asar: { - unpackDir: "files" - }, - icon:'./src/icons/icon.ico', + packagerConfig: { + asar: { + unpackDir: "files", }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', config: {}, - }, - { - name: '@electron-forge/maker-zip', platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', config: {}, + icon: "./src/icons/icon.ico", + }, + rebuildConfig: {}, + makers: [ + { + name: "@electron-forge/maker-squirrel", + config: {}, + }, + { + name: "@electron-forge/maker-zip", + platforms: ["darwin"], + }, + { + name: "@electron-forge/maker-deb", + config: {}, + }, + { + name: "@electron-forge/maker-rpm", + config: {}, + }, + ], + plugins: [ + // { + // name: '@electron-forge/plugin-auto-unpack-natives', + // config: {}, + // }, + { + name: "@electron-forge/plugin-webpack", + config: { + mainConfig: "./webpack.main.config.js", + devContentSecurityPolicy: "media-src file:", + renderer: { + config: "./webpack.renderer.config.js", + entryPoints: [ + { + html: "./src/renderer/presenterView/index.html", + css: "./src/renderer/presenterView/index.scss", + js: "./src/renderer/presenterView/renderer.js", + name: "main_window", + preload: { + js: "./src/renderer/preload.js", + }, + }, + { + html: "./src/renderer/openFileDialog/index.html", + js: "./src/renderer/openFileDialog/index.js", + name: "open_file_dialog", + preload: { + js: "./src/renderer/preload.js", + }, + }, + { + html: "./src/renderer/saveFileDialog/index.html", + js: "./src/renderer/saveFileDialog/index.js", + name: "save_file_dialog", + preload: { + js: "./src/renderer/preload.js", + }, + }, + { + html: "./src/renderer/presentationView/index.html", + js: "./src/renderer/presentationView/renderer.js", + name: "presentation_view", + preload: { + js: "./src/renderer/preloadPresentationView.js", + }, + }, + { + html: "./src/renderer/ShowCreateView/index.html", + js: "./src/renderer/ShowCreateView/renderer.js", + name: "show_creator_view", + preload: { + js: "./src/renderer/preload.js", + }, + }, + ], }, - { - name: '@electron-forge/maker-rpm', config: {}, + }, + }, + ], + publishers: [ + { + name: "@electron-forge/publisher-github", + config: { + repository: { + owner: "marksamfd", + name: "VideoSlideshow", }, - ], plugins: [// { - // name: '@electron-forge/plugin-auto-unpack-natives', - // config: {}, - // }, - { - name: '@electron-forge/plugin-webpack', - config: { - mainConfig: './webpack.main.config.js', - devContentSecurityPolicy: "media-src file:", - renderer: { - config: './webpack.renderer.config.js', - entryPoints: [ - { - html: './src/renderer/presenterView/index.html', - css: './src/renderer/presenterView/index.scss', - js: './src/renderer/presenterView/renderer.js', - name: 'main_window', - preload: { - js: './src/renderer/preload.js' - } - }, - { - html: './src/renderer/openFileDialog/index.html', - js: './src/renderer/openFileDialog/index.js', - name: 'open_file_dialog', - preload: { - js: './src/renderer/preload.js' - } - }, - { - html: './src/renderer/saveFileDialog/index.html', - js: './src/renderer/saveFileDialog/index.js', - name: 'save_file_dialog', - preload: { - js: './src/renderer/preload.js' - } - }, { - html: './src/renderer/presentationView/index.html', - js: './src/renderer/presentationView/renderer.js', - name: 'presentation_view', - preload: { - js: './src/renderer/preloadPresentationView.js' - } - }, { - html: './src/renderer/ShowCreateView/index.html', - js: './src/renderer/ShowCreateView/renderer.js', - name: 'show_creator_view', - preload: { - js: './src/renderer/preload.js' - } - }] - }, - - } - }], - publishers: [{ - name: '@electron-forge/publisher-github', - config: { - repository: { - owner: 'marksamfd', - name: 'VideoSlideshow' - }, - prerelease: false - } - }] + prerelease: false, + }, + }, + ], }; diff --git a/package-lock.json b/package-lock.json index 9fc5be9..203ed44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,18 @@ { "name": "choirslides", - "version": "2.0.0", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "choirslides", - "version": "2.0.0", + "version": "2.0.6", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@popperjs/core": "^2.11.8", + "@types/archiver": "^6.0.3", + "@types/yazl": "^3.3.0", "adm-zip": "^0.5.14", "archiver": "^7.0.1", "bootstrap": "^5.3.3", @@ -19,12 +21,21 @@ "electron-log": "^5.1.5", "electron-reload": "^2.0.0-alpha.1", "electron-squirrel-startup": "^1.0.0", + "font-list": "^1.5.1", "fswin": "^3.24.524", "hotkeys-js": "^3.13.7", "interactjs": "^1.10.27", "konva": "^9.3.6", + "mime": "^4.0.4", "node-fetch": "^3.3.2", - "unzipper": "^0.12.1" + "node-stream-zip": "^1.15.0", + "progress-stream": "^2.0.0", + "tar": "^7.4.3", + "tar-stream": "^3.1.7", + "unzipper": "^0.12.1", + "video.js": "^8.17.4", + "videojs": "^1.0.0", + "yazl": "^3.3.1" }, "devDependencies": { "@electron-forge/cli": "^7.3.1", @@ -35,6 +46,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", "@electron-forge/plugin-webpack": "^7.3.1", "@electron-forge/publisher-github": "^6.3.0", + "@types/tar-stream": "^3.1.4", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "autoprefixer": "^10.4.19", "copy-webpack-plugin": "^12.0.2", @@ -46,7 +58,10 @@ "sass": "^1.74.1", "sass-loader": "^14.1.1", "style-loader": "^3.3.4", - "svg-inline-loader": "^0.8.2" + "svg-inline-loader": "^0.8.2", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" } }, "node_modules/@babel/code-frame": { @@ -157,6 +172,41 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@electron-forge/cli": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.3.1.tgz", @@ -831,6 +881,34 @@ "node": ">=12.13.0" } }, + "node_modules/@electron/rebuild/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/universal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", @@ -1041,6 +1119,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1414,6 +1513,43 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/archiver": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz", + "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==", + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1594,7 +1730,6 @@ "version": "20.12.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1625,6 +1760,15 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -1679,6 +1823,16 @@ "@types/node": "*" } }, + "node_modules/@types/tar-stream": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz", + "integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -1698,6 +1852,15 @@ "@types/node": "*" } }, + "node_modules/@types/yazl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-3.3.0.tgz", + "integrity": "sha512-mFL6lGkk2N5u5nIxpNV/K5LW3qVSbxhJrMxYGOOxZndWxMgCamr/iCsq/1t9kd8pEwhuNP91LC5qZm/qS9pOEw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vercel/webpack-asset-relocator-loader": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@vercel/webpack-asset-relocator-loader/-/webpack-asset-relocator-loader-1.7.3.tgz", @@ -1707,6 +1870,77 @@ "resolve": "^1.10.0" } }, + "node_modules/@videojs/http-streaming": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.3.tgz", + "integrity": "sha512-L7H+iTeqHeZ5PylzOx+pT3CVyzn4TALWYTJKkIc1pDaV/cTVfNGtG+9/vXPAydD+wR/xH1M9/t2JH8tn/DCT4w==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "4.0.0", + "aes-decrypter": "4.0.1", + "global": "^4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.3.0", + "mux.js": "7.0.3", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.14.0" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", + "integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -1857,7 +2091,6 @@ "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -1877,8 +2110,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -1925,6 +2157,19 @@ "acorn": "^8" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adm-zip": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.14.tgz", @@ -1933,6 +2178,30 @@ "node": ">=12.0" } }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2015,6 +2284,14 @@ "ajv": "^8.8.2" } }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "engines": { + "node": ">=0.4.2" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2086,6 +2363,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", @@ -2276,6 +2554,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2757,6 +3042,34 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -2822,6 +3135,14 @@ "tslib": "^2.0.3" } }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001606", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz", @@ -3054,6 +3375,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha512-QjQ1T4BqyHv19k6XSfdhy/QLlIOhywz0ekBUCa9h71zYMJlfDTGan/Z1JXzYkZ6v8R+GhvL/p4FZPbPW8WNXlg==", + "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", + "peer": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3085,6 +3420,15 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==", + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3400,6 +3744,13 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -3571,6 +3922,15 @@ "node": ">= 12" } }, + "node_modules/dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha512-AXvW8g7tO4ilk5HgOWeDmPi/ZPaCnMJ+9Cg1I3p19w6mcvAAXBuuGEXAxybC+Djj1PSZUiHUcyoYu7WneCX8gQ==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3588,6 +3948,14 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -3797,6 +4165,16 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -3842,6 +4220,11 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -4779,6 +5162,19 @@ "node": ">=8.0.0" } }, + "node_modules/esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -4826,6 +5222,12 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "peer": true + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -4937,6 +5339,15 @@ "which": "bin/which" } }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -5299,6 +5710,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha512-yjftfYnF4ThYEvKEV/kEFR15dmtyXTAh3vQnzpJUoc7Naj5y1P0Ck7Zs1+Vroa00E3KT3IYsk756S+8WA5dNLw==", + "peer": true, + "dependencies": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/findup-sync/node_modules/glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, + "dependencies": { + "inherits": "2", + "minimatch": "0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/findup-sync/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/findup-sync/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "node_modules/findup-sync/node_modules/minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "peer": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -5341,6 +5809,12 @@ } } }, + "node_modules/font-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/font-list/-/font-list-1.5.1.tgz", + "integrity": "sha512-Hr5V0dsSv91wH3FgirXd7qh1PydqA/vMQyWjFFWn+lUPJtC+3i2tzgVqbLRcvQh87TGdbTGbAR3mEo4VlwC1jw==", + "license": "MIT" + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -5614,6 +6088,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha512-hIGEBfnHcZpWkXPsAVeVmpYDvfy/matVl03yOY91FPmnpCC12Lm5izNxCjO3lHAeO6uaTwMxu7g450Siknlhig==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5652,6 +6135,15 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -5790,9 +6282,328 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "node_modules/grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha512-1iq3ylLjzXqz/KSq1OAE2qhnpcbkF2WyhsQcavZt+YmgvHu0EbPMEhGhy2gr0FP67isHpRdfwjB5WVeXXcJemQ==", + "peer": true, + "dependencies": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-contrib-uglify": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.2.7.tgz", + "integrity": "sha512-KXKM2UNLsCiUI6/DYfAIPm3i26UJJN6Cf6KD8fFa2TKllj7yLPC853IxtWBJ/3jX66QtXHGtdCORuuA6sAFvvA==", + "dependencies": { + "grunt-lib-contrib": "~0.6.1", + "uglify-js": "~2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "peerDependencies": { + "grunt": "~0.4.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha512-qYs/uM0ImdzwIXLhS4O5WLV5soAM+PEqqHI/hzSxlo450ERSccEhnXqoeDA9ZozOdaWuYnzTOTwRcVRogleMxg==", + "peer": true, + "dependencies": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha512-D0vbUX00TFYCKNZtcZzemMpwT8TR/FdRs1pmfiBw6qnUw80PfsjV+lhIozY/3eJ3PSG2zj89wd2mH/7f4tNAlw==", + "peer": true, + "dependencies": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-log-utils/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-log/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-log/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha512-cXPbfF8aM+pvveQeN1K872D5fRm30xfJWZiS63Y8W8oyIPLClCsmI8bW96Txqzac9cyL4lRqEBhbhJ3n5EzUUQ==", + "peer": true, + "dependencies": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-util/node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-util/node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha512-LVbt/rjK62gSbhehDVKL0vlaime4Y1IBixL+bKeNfoY4L2zab/jGrxU6Ka05tMA/zBxkTk5t3ivtphdyYupczw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-util/node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha512-E87fdQ/eRJr9W1X4wTPejNy9zTW3FI2vpCZSJ/HAY+TkjKVC0TUm1jk6vn2Z7qay0DQy0+RBGdXxj+RmmiGZKQ==", + "peer": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/grunt-lib-contrib": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-lib-contrib/-/grunt-lib-contrib-0.6.1.tgz", + "integrity": "sha512-HdCtJuMmmkSAVrAfsG7lZWE0YabrsPWwzcCCUgWQOAaQsQSUNhw/IwD2YjCSLh5y9NXSPzHTYFLL4ro7QbAJMA==", + "dependencies": { + "zlib-browserify": "0.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt/node_modules/argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha512-LjmC2dNpdn2L4UzyoaIr11ELYoLn37ZFy9zObrQFHsSuOepeUEMKnM8w5KL4Tnrp2gy88rRuQt6Ky8Bjml+Baw==", + "peer": true, + "dependencies": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + } + }, + "node_modules/grunt/node_modules/argparse/node_modules/underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha512-yxkabuCaIBnzfIvX3kBxQqCs0ar/bfJwDnFEHJUm/ZrRVhT3IItdRF5cZjARLzEnyQYtIUhsZ2LG2j3HidFOFQ==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, + "dependencies": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ==", + "deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/grunt/node_modules/iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/grunt/node_modules/inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA==", + "peer": true + }, + "node_modules/grunt/node_modules/js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha512-VEKcIksckDBUhg2JS874xVouiPkywVUh4yyUmLCDe1Zg3bCd6M+F1eGPenPeHLc2XC8pp9G8bsuofK0NeEqRkA==", + "peer": true, + "dependencies": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/grunt/node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha512-LVbt/rjK62gSbhehDVKL0vlaime4Y1IBixL+bKeNfoY4L2zab/jGrxU6Ka05tMA/zBxkTk5t3ivtphdyYupczw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "node_modules/grunt/node_modules/minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "peer": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "peer": true, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/grunt/node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha512-E87fdQ/eRJr9W1X4wTPejNy9zTW3FI2vpCZSJ/HAY+TkjKVC0TUm1jk6vn2Z7qay0DQy0+RBGdXxj+RmmiGZKQ==", + "peer": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, @@ -5880,6 +6691,15 @@ "node": ">=0.10.0" } }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -6381,6 +7201,11 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6664,9 +7489,9 @@ } }, "node_modules/konva": { - "version": "9.3.6", - "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.6.tgz", - "integrity": "sha512-dqR8EbcM0hjuilZCBP6xauQ5V3kH3m9kBcsDkqPypQuRgsXbcXUrxqYxhNbdvKZpYNW8Amq94jAD/C0NY3qfBQ==", + "version": "9.3.22", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.22.tgz", + "integrity": "sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==", "funding": [ { "type": "patreon", @@ -6680,7 +7505,8 @@ "type": "github", "url": "https://github.com/sponsors/lavrton" } - ] + ], + "license": "MIT" }, "node_modules/launch-editor": { "version": "2.6.1", @@ -6959,6 +7785,36 @@ "node": ">=12" } }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, + "node_modules/m3u8-parser/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -7090,15 +7946,17 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { @@ -7140,6 +7998,14 @@ "node": ">=4" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -7269,6 +8135,20 @@ "node": ">=10" } }, + "node_modules/mpd-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", + "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7288,6 +8168,22 @@ "multicast-dns": "cli.js" } }, + "node_modules/mux.js": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz", + "integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -7427,6 +8323,34 @@ "node": "^12.13 || ^14.13 || >=16" } }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7457,6 +8381,18 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -8014,6 +8950,17 @@ "node": ">=0.10.0" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8285,6 +9232,16 @@ "node": ">=0.4.0" } }, + "node_modules/progress-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz", + "integrity": "sha512-xJwOWR46jcXUq6EH9yYyqp+I52skPySOeHfkxOZ2IY1AiBi/sFJhbhAKHoV3OTw/omQ45KTio9215dRJ2Yxd3Q==", + "license": "BSD-2-Clause", + "dependencies": { + "speedometer": "~1.0.0", + "through2": "~2.0.3" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -8623,6 +9580,11 @@ "node": ">= 10.13.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -9056,6 +10018,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9280,6 +10254,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "peer": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9457,6 +10437,12 @@ "wbuf": "^1.7.3" } }, + "node_modules/speedometer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz", + "integrity": "sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==", + "license": "MIT" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -9724,39 +10710,85 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/temp": { @@ -9920,6 +10952,46 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -10000,6 +11072,81 @@ "node": ">=0.8.0" } }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -10031,11 +11178,88 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha512-tktIjwackfZLd893KGJmXc1hrRHH1vH9Po3xFh1XBjjeGAnN02xJ3SuoA+n1L29/ZaCA18KzCFlckS+vfPugiA==", + "dependencies": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/uglify-js/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/uglify-js/node_modules/source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha512-yfCwDj0vR9RTwt3pEzglgb3ZgmcXHt6DjG3bjJvzPwTL+5zDQ2MhmSzAcTy0GTiQuCiriSWXvWM1/NhKdXuoQA==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-js/node_modules/yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha512-5j382E4xQSs71p/xZQsU1PtRA2HXPAjX0E0DkoGLxwNASMOKX6A9doV1NrZmj85u2Pjquz402qonBzz/yLPbPA==", + "dependencies": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==" + }, + "node_modules/underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==", + "peer": true + }, + "node_modules/underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha512-3FVmhXqelrj6gfgp3Bn6tOavJvW0dNH2T+heTD38JRxIrAbiuzbqjknszoOYj3DyFB1nWiLj208Qt2no/L4cIA==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -10165,6 +11389,11 @@ "punycode": "^2.1.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "node_modules/username": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", @@ -10207,6 +11436,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -10226,6 +11462,62 @@ "node": ">= 0.8" } }, + "node_modules/video.js": { + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.4.tgz", + "integrity": "sha512-AECieAxKMKB/QgYK36ci50phfpWys6bFT6+pGMpSafeFYSoZaQ2Vpl83T9Qqcesv4TO7oNtiycnVeaBnrva2oA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "3.13.3", + "@videojs/vhs-utils": "^4.0.0", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.1", + "global": "4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.2.2", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/videojs/-/videojs-1.0.0.tgz", + "integrity": "sha512-FwI02jJ7d4E6goWuc/4LTN5OJlD1M0jInoIoNemo4EzMfu6IywhahMXDriLObX17ML62RsHS0oiCUE9wVB6i8A==", + "deprecated": "This is a placeholder package, please use the official 'video.js' package", + "dependencies": { + "grunt-contrib-uglify": "^0.2.7" + } + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==" + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "dependencies": { + "global": "^4.3.1" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -10564,6 +11856,14 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -10574,6 +11874,14 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10644,6 +11952,15 @@ "node": ">=8.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/xterm": { "version": "4.19.0", "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", @@ -10807,6 +12124,34 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yazl": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz", + "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "node_modules/yazl/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -10869,6 +12214,11 @@ "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } + }, + "node_modules/zlib-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.1.tgz", + "integrity": "sha512-fheIDCKXU0YAGZMv4FFwVTBMQRSv2ZjNqRN1VkZjetZDK/BC/hViEhasTh0kTeogcsIAl5gYE04GN53trT+cFw==" } } } diff --git a/package.json b/package.json index e92caca..b8899bd 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@popperjs/core": "^2.11.8", + "@types/archiver": "^6.0.3", + "@types/yazl": "^3.3.0", "adm-zip": "^0.5.14", "archiver": "^7.0.1", "bootstrap": "^5.3.3", @@ -33,12 +35,21 @@ "electron-log": "^5.1.5", "electron-reload": "^2.0.0-alpha.1", "electron-squirrel-startup": "^1.0.0", + "font-list": "^1.5.1", "fswin": "^3.24.524", "hotkeys-js": "^3.13.7", "interactjs": "^1.10.27", "konva": "^9.3.6", + "mime": "^4.0.4", "node-fetch": "^3.3.2", - "unzipper": "^0.12.1" + "node-stream-zip": "^1.15.0", + "progress-stream": "^2.0.0", + "tar": "^7.4.3", + "tar-stream": "^3.1.7", + "unzipper": "^0.12.1", + "video.js": "^8.17.4", + "videojs": "^1.0.0", + "yazl": "^3.3.1" }, "devDependencies": { "@electron-forge/cli": "^7.3.1", @@ -49,6 +60,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", "@electron-forge/plugin-webpack": "^7.3.1", "@electron-forge/publisher-github": "^6.3.0", + "@types/tar-stream": "^3.1.4", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "autoprefixer": "^10.4.19", "copy-webpack-plugin": "^12.0.2", @@ -60,6 +72,12 @@ "sass": "^1.74.1", "sass-loader": "^14.1.1", "style-loader": "^3.3.4", - "svg-inline-loader": "^0.8.2" - } + "svg-inline-loader": "^0.8.2", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + }, + "extraResources": [ + "./extraResources/**" + ] } diff --git a/src/index.js b/src/index.js index 8614d0c..8f52379 100644 --- a/src/index.js +++ b/src/index.js @@ -1,330 +1,244 @@ -const {app, BrowserWindow, ipcMain, dialog, protocol, net} = require('electron'); -const path = require('path'), fs = require("fs") -const {pathToFileURL} = require('url') +const { app, BrowserWindow, ipcMain, dialog, protocol } = require("electron"); +const path = require("path"); const electron = require("electron"); -import {createPresentationView, createShowCreatorView, createPresenterView} from './createViews' -import WorkingFile from './workingFile' -import log from 'electron-log/main'; +import { + createPresentationView, + createShowCreatorView, + createPresenterView, +} from "./createViews"; +import MediaResponder from "./utils/MediaResponderClass"; +import WorkingFile from "./workingFile"; -let VidFilestream; +import log from "electron-log/main"; +import cp from "child_process"; + +const EXTRARESOURCES_PATH = app.isPackaged + ? path.join(process.resourcesPath, "extraResources") + : path.join(__dirname, "../../extraResources"); + +const getExtraResourcesPath = (...paths) => { + return path.join(EXTRARESOURCES_PATH, ...paths); +}; if (app.isPackaged) { - log.initialize({spyRendererConsole: true}); - log.transports.file.format = '[{h}:{i}:{s}.{ms}] [{processType}] {text}'; -// log.transports.console.level = false; - Object.assign(console, log.functions); + log.initialize({ spyRendererConsole: true }); + log.transports.file.format = "[{h}:{i}:{s}.{ms}] [{processType}] {text}"; + // log.transports.console.level = false; + Object.assign(console, log.functions); } /** * The open project Now * @type {WorkingFile} */ -let currentProject = new WorkingFile({filePath: pathToFileURL(process.cwd())}); +let currentProject; let presentationView, presenterView, showCreatorView; // Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (require('electron-squirrel-startup')) { - app.quit(); +if (require("electron-squirrel-startup")) { + app.quit(); } protocol.registerSchemesAsPrivileged([ - { - scheme: "media", - privileges: { - // secure: true, - bypassCSP: true, - stream: true, - }, + { + scheme: "media", + privileges: { + secure: true, + bypassCSP: true, + stream: true, + supportFetchAPI: true, + standard: true, }, -]) - -app.disableHardwareAcceleration() - -function parseRangeRequests(text, size) { - const token = text.split("="); - if (token.length !== 2 || token[0] !== "bytes") { - return []; - } - - return token[1] - .split(",") - .map((v) => parseRange(v, size)) - .filter(([start, end]) => !isNaN(start) && !isNaN(end) && start <= end); -} - -const NAN_ARRAY = [NaN, NaN]; - -function parseRange(text, size) { - const token = text.split("-"); - if (token.length !== 2) { - return NAN_ARRAY; - } - - const startText = token[0].trim(); - const endText = token[1].trim(); - - if (startText === "") { - if (endText === "") { - return NAN_ARRAY; - } else { - let start = size - Number(endText); - if (start < 0) { - start = 0; - } + }, +]); - return [start, size - 1]; - } - } else { - if (endText === "") { - return [Number(startText), size - 1]; - } else { - let end = Number(endText); - if (end >= size) { - end = size - 1; - } - - return [Number(startText), end]; - } - } -} +app.disableHardwareAcceleration(); +let canQuit = false; // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.on('ready', () => { - protocol.handle("media", (request) => { - // https://github.com/electron/electron/issues/38749#issuecomment-1681531939 - const fp = path.join(currentProject.basePath, decodeURIComponent(request.url.slice('media://'.length))) - const stats = fs.statSync(fp); - - // console.log(fp, stats) - const headers = new Headers(); - headers.set("Accept-Ranges", "bytes"); - headers.set("Content-Type", "video/mp4"); - - let status = 200; - const rangeText = request.headers.get("range"); - - if (rangeText) { - const ranges = parseRangeRequests(rangeText, stats.size); - - const [start, end] = ranges[0]; - // console.log(rangeText, stats.size, start, end); - headers.set("Content-Length", `${end - start + 1}`); - headers.set("Content-Range", `bytes ${start}-${end}/${stats.size}`); - status = 206; - VidFilestream = fs.createReadStream(fp, {start, end}); - } else { - headers.set("Content-Length", `${stats.size}`); - VidFilestream = fs.createReadStream(fp); - - } - - return new Response(VidFilestream, { - headers, - status, - }); - }) - - showCreatorView = createShowCreatorView() - showCreatorView.on('close', (e) => { - if (currentProject.isOpened) { - let choice = dialog.showMessageBoxSync(showCreatorView, - { - type: 'question', - title: 'Save your Work', - message: 'Make sure to save your work before Quitting \nAre you sure you want to Quit?', - buttons: ['Yes', 'No'], - }) - if (choice === 1) { - e.preventDefault() - } else { - if (currentProject.isOpened) { - VidFilestream?.destroy() - currentProject.closeProject() - } - } - } - }) +app.on("ready", () => { + protocol.handle("media", async (request) => { + const responder = new MediaResponder(request, currentProject); + return await responder.handle(); + + // https://github.com/electron/electron/issues/38749#issuecomment-1681531939 + }); + + showCreatorView = createShowCreatorView(); + showCreatorView.on("close", (e) => { + if (currentProject.isOpened && !canQuit) { + e.preventDefault(); // stop immediate close + showCreatorView.webContents.send("save-before-quit"); + } + }); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. -app.on('window-all-closed', () => { - - if (process.platform !== 'darwin') { - app.quit(); - } +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } }); - -app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - createPresenterView(); - } +app.on("activate", () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createPresenterView(); + } }); ipcMain.handle("file-dialog-open", (e, mode) => { - let filePath - let fileFilters = [{name: "ChoirSlide Files", extensions: ["chs"]}] - if (mode === "o") { - filePath = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), { - properties: ["openFile"], filters: fileFilters - }) - } else if (mode === "s") { - filePath = dialog.showSaveDialogSync(BrowserWindow.getFocusedWindow(), { - properties: ["openFile"], filters: fileFilters - }) - } - return filePath ? filePath : "" - -}) + let filePath; + let fileFilters = [{ name: "ChoirSlide Files", extensions: ["chs", "json"] }]; + if (mode === "o") { + filePath = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), { + properties: ["openFile"], + filters: fileFilters, + }); + } else if (mode === "s") { + filePath = dialog.showSaveDialogSync(BrowserWindow.getFocusedWindow(), { + properties: ["openFile"], + filters: fileFilters, + }); + } + return filePath ? filePath : ""; +}); ipcMain.handle("file-opened", async (e, data) => { - let mainWindow = BrowserWindow.getFocusedWindow().getParentWindow(); - BrowserWindow.getFocusedWindow().destroy() + let mainWindow = BrowserWindow.getFocusedWindow().getParentWindow(); + BrowserWindow.getFocusedWindow().destroy(); + currentProject = new WorkingFile({ ...data }); + if (!data.present) { + await currentProject.editProject(); - if (currentProject.isOpened) { - VidFilestream?.destroy() - mainWindow.webContents.send("slideshow:destroy") - currentProject.closeProject() - } - - currentProject = new WorkingFile(data) - - if (!fs.existsSync(data["filePath"])) { - currentProject.createProject() - } else { - currentProject.openProject() - } - mainWindow.setTitle(`ChoirSlide - ${currentProject.projectName}`) + mainWindow.setTitle(`ChoirSlide - ${currentProject.projectName}`); mainWindow.webContents.send("file-params", currentProject.toObject()); -}) + return; + } + await currentProject.presentProject(); + initPresentationView(); +}); ipcMain.handle("file-save", (e, content) => { - dialog.showMessageBox(BrowserWindow.fromId(e.frameId), { - title: "File Save", - message: "File Is Saving", - type: "info", - }) - - currentProject.saveProject(content).then(() => { - dialog.showMessageBox(BrowserWindow.fromId(e.frameId), { - title: "File Save", - message: "File Saved Successfully", - type: "info" - }) - }).catch(err => { - dialog.showMessageBoxSync({ - title: "File Save", - message: `An error occurred during saving the file \n ${err}`, - type: "info" - }) - }) -}) - -ipcMain.handle("create-thumb", (e, props) => { - const base64Data = props.pic.replace(/^data:image\/png;base64,/, ""); - return fs.writeFileSync(`${currentProject.basePath}/${props.filename}.png`, base64Data, 'base64'); -}) - -ipcMain.handle("copy-video", (e, videoOriginalPath) => { - console.log(videoOriginalPath) - let videoFileName = path.basename(videoOriginalPath) - return fs.copyFileSync(`${videoOriginalPath}`, `${currentProject.basePath}/${videoFileName}`) -}) + dialog.showMessageBox(BrowserWindow.fromId(e.frameId), { + title: "File Save", + message: "File Is Saving", + type: "info", + }); + currentProject.saveProject(content); +}); ipcMain.handle("save-quit", async (e, content) => { - log.info("Save and quit IPC") - /*if (JSON.stringify(workingFile) !== "{}") { - let choice = dialog.showMessageBoxSync(BrowserWindow.fromWebContents(e.sender), - { - type: 'question', - title: 'Save your Work', - message: 'Do you want to save your work before quitting', - buttons: ['Yes', 'No'], - }); - if (choice === 0) { - if (fs.existsSync(workingFile["basePath"])) { - try { - await saveShow(content) - fs.rmSync(path.join(workingFile['directory'], `${projectRandom}.json`)) - fs.rmSync(workingFile["basePath"], {recursive: true, force: true}); - dialog.showMessageBoxSync(BrowserWindow.fromWebContents(e.sender), { - title: "File Save", - message: "File Saved Successfully", - type: "info" - }) - showCreatorView.destroy() - app.quit() - } catch (err) { - dialog.showMessageBoxSync(BrowserWindow.fromWebContents(e.sender), { - title: "File Save", - message: `An error occurred during saving the file \n ${err}`, - type: "error" - }) - } - } else { - showCreatorView.destroy() - app.quit() - } - } else { - showCreatorView.destroy() - app.quit() - } - }*/ -}) + console.log({ e, content }); + return currentProject.closeProject(content); +}); -ipcMain.handle("slideshow:start", (e, content) => { +ipcMain.on("save-done", () => { + canQuit = true; + if (showCreatorView) { + showCreatorView.close(); + } +}); - let choice = dialog.showMessageBoxSync(showCreatorView, - { - type: 'question', - title: 'Save your Work', - message: 'Please make sure that you have saved the show before starting \nAre you sure you want to continue ?', - buttons: ['Yes', 'No'], - }); - if (choice === 0) { - let displays = electron.screen.getAllDisplays() - const externalDisplay = displays.find((display) => { - return display.bounds.x !== 0 || display.bounds.y !== 0 - }) +ipcMain.handle("getSystemFonts", async () => { + const systemFontsScriptPath = getExtraResourcesPath( + "fontlist/getSystemFonts.js" + ); + console.log(systemFontsScriptPath); + return new Promise((resolve, reject) => { + const forked = cp.fork(systemFontsScriptPath); + + forked.on("message", (message) => { + resolve(message); + forked.kill(); + }); + + forked.on("error", (err) => { + reject(err); + }); + + forked.on("exit", (code) => { + if (code !== 0) { + reject(new Error(`getSystemFonts.js exited with code ${code}`)); + } + }); + }); +}); - let data = currentProject.toObject() - if (externalDisplay) { - presenterView = createPresenterView() - presenterView.webContents.once('dom-ready', () => { - presenterView.webContents.send("file-params", data) - }) +ipcMain.on( + "addSlideFiles", + (_, { imgBase64, imgFileName, videoFilePath, videoFileName }) => { + const base64Data = imgBase64.replace(/^data:image\/png;base64,/, ""); + let imgBuffer = Buffer.from(base64Data, "base64"); + currentProject.addVideoSlideFiles({ + imgBuffer, + imgFileName, + videoFilePath, + videoFileName, + }); + } +); + +function initPresentationView() { + let displays = electron.screen.getAllDisplays(); + const externalDisplay = displays.find((display) => { + return display.bounds.x !== 0 || display.bounds.y !== 0; + }); + + let data = currentProject.toObject(); + console.log(data); + if (externalDisplay) { + presenterView = createPresenterView(); + presenterView.webContents.once("dom-ready", () => { + presenterView.webContents.send("file-params", data); + }); + + presentationView = createPresentationView( + presenterView, + externalDisplay.bounds.x, + externalDisplay.bounds.y + ); + presentationView.webContents.once("dom-ready", () => { + presentationView.webContents.send("main:presentation", { + type: "init", + data, + }); + }); + showCreatorView.destroy(); + presenterView.focus(); + } else { + dialog.showMessageBoxSync(showCreatorView, { + type: "error", + title: "No Second Screen Detected", + message: + "Please make sure to connect another screen and the projection mode is set to Extend", + }); + } +} - presenterView.on('close', () => { - currentProject.closeProject() - }) - presentationView = createPresentationView(presenterView, externalDisplay.bounds.x, externalDisplay.bounds.y) - presentationView.webContents.once('dom-ready', () => { - presentationView.webContents.send("main:presentation", {type: "init", data}) - }) - showCreatorView.destroy() - presenterView.focus() - } else { - dialog.showMessageBoxSync(showCreatorView, { - type: 'error', - title: "No Second Screen Detected", - message: "Please make sure to connect another screen and the projection mode is set to Extend" - }) - } - } -}) +ipcMain.handle("slideshow:start", (e, content) => { + let choice = dialog.showMessageBoxSync(showCreatorView, { + type: "question", + title: "Save your Work", + message: + "Please make sure that you have saved the show before starting \nAre you sure you want to continue ?", + buttons: ["Yes", "No"], + }); + if (choice === 0) { + initPresentationView(); + } +}); //presenter:main //main:presentation ipcMain.on("to-presentation", (e, msg) => { - // console.log(msg) - presentationView?.webContents.send("main:presentation", msg) -}) - + // console.log(msg) + presentationView?.webContents.send("main:presentation", msg); +}); diff --git a/src/renderer/ShowCreateView/index.html b/src/renderer/ShowCreateView/index.html index 26d3ae0..0a0c163 100644 --- a/src/renderer/ShowCreateView/index.html +++ b/src/renderer/ShowCreateView/index.html @@ -1,85 +1,410 @@ - - - + + ChoirSlides - - - -
-
-
-
-
- -
-
+ + + + +
+
+ +
+ +
+
+
+
+
+ videocam + Video Preview +
+
+ + + +
+
+ +
+ + +
+
+
+
+ +
+
+
+
+
+
+ text_fields + Slide Text Editor +
+
+ + +
+ + + + + +
+ + +
+
+
+
+ +
+
+
- +
+
+
+ +
+ add_circle +

+ No slides yet. Click "Add New Slide" to get started. +

+
+ +
-
    - -
+
+ + info + Total slides: 0 + +
+
+
-
- - - - \ No newline at end of file + + diff --git a/src/renderer/ShowCreateView/js/fileOpen.js b/src/renderer/ShowCreateView/js/fileOpen.js index c7adced..1b28f44 100644 --- a/src/renderer/ShowCreateView/js/fileOpen.js +++ b/src/renderer/ShowCreateView/js/fileOpen.js @@ -1,55 +1,61 @@ -import Slide from "../../js/Classes/SlideClass"; -import ShowCreator from "../../js/Classes/ShowCreatorClass"; +import Slide from "../../js/Classes/Slide"; +import ShowCreator from "../../js/Classes/ShowCreator"; import hotkeys from "hotkeys-js"; -let present +/** + * @type {ShowCreator} + */ +let present; window.file.onFileParams(function (fileParams) { - console.log(fileParams); - let presentation = JSON.parse(fileParams["content"]) - let slides = presentation.map(e => new Slide(e)) - let slidePreviewCanv = document.getElementById("currentSlideThumbCanvas"); - - // comm.toPresentation({type: "init", data: JSON.stringify(fileParams)}) - present = new ShowCreator({ - container: "currentSlideThumbCanvas", - sidebarSlidesContainer: document.getElementById("sidebarSlidesContainer"), - lyricsContainer: document.getElementById("textSlidesList"), - slideTextEditor: document.getElementById("textSlidesEditor"), - width: slidePreviewCanv.clientWidth, - height: slidePreviewCanv.clientHeight, - slides, - basePath: fileParams.basePath, - mode: fileParams.mode, - sepBy: fileParams.sepBy - }) - - // window.onbeforeunload = (e) => { - // e.preventDefault() - // file.saveAndQuit(present.saveShow()) - // - // } - - -}) -hotkeys('delete,ctrl+s', function (event, handler) { - switch (handler.key) { - case 'delete': - present?.removeSlide() - break; - case 'ctrl+s': - file.save(present.saveShow()) + console.log(fileParams); + let presentation = JSON.parse(fileParams["content"]); + let slides = presentation.map((e) => new Slide(e)); + let slidePreviewCanv = document.getElementById("currentSlideThumbCanvas"); + + present = new ShowCreator({ + slides: [...slides], + sidebarSlidesContainer: document.getElementById("sidebarSlidesContainer"), + container: "currentSlideThumbCanvas", + width: slidePreviewCanv.clientWidth, + height: slidePreviewCanv.clientHeight, + splitStrategy: fileParams.mode, + splitDelimiter: fileParams.sepBy, + addSlideBtn: document.querySelector(`#slideAdd`), + removeSlideBtn: document.querySelector(`#slideDelete`), + textEditorField: document.querySelector("textarea"), + fontSelector: document.querySelector("#fontSelector"), + backgroundEnabledBtn: document.querySelector("#backgroundEnabledBtn"), + videoToolbar: document.querySelector("#videoToolbar"), + }); + + window.file.onSaveBeforeQuit(async () => { + console.log("Saving data before quitting..."); + if (await file.saveAndQuit(present.stringifyShow())) { + window.file.saveDone(); } -}) + }); +}); +hotkeys("delete,ctrl+s", function (event, handler) { + switch (handler.key) { + case "delete": + present?.removeSlide(); + break; + case "ctrl+s": + file.save(present.stringifyShow()); + console.log(present.stringifyShow()); + } +}); window.comm.onSlideshowInitialized(() => { - comm.startSlideshow(present.saveShow()) -}) - -window.comm.onSlideshowDestroy(() => { + comm.startSlideshow(present.stringifyShow()); +}); + +/* window.comm.onSlideshowDestroy(() => { + present?.destroyCreator(); +}); +window.onbeforeunload = () => { + console.log("Destroying show") present?.destroyCreator() -}) -// window.onbeforeunload = () => { -// console.log("Destroying show") -// present?.destroyCreator() -// } \ No newline at end of file +} + */ diff --git a/src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..a98aa97 --- /dev/null +++ b/src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/css/_style.scss b/src/renderer/css/_style.scss index 6ce250d..bf2dc6d 100644 --- a/src/renderer/css/_style.scss +++ b/src/renderer/css/_style.scss @@ -1,14 +1,14 @@ @font-face { - font-family: 'Material Symbols Outlined'; + font-family: "Material Symbols Outlined"; font-style: normal; - src: url(../asset/resource/MaterialSymbolsRounded.woff2) format('woff'); + src: url(../asset/resource/MaterialSymbolsRounded.woff2) format("woff"); } .material-symbols-outlined { - font-family: 'Material Symbols Outlined'; + font-family: "Material Symbols Outlined"; font-weight: normal; font-style: normal; - font-size: 1.5em; /* Preferred icon size */ + font-size: 1.5em; /* Preferred icon size */ display: inline-block; line-height: 1; text-transform: none; @@ -19,7 +19,7 @@ } body { - margin: .5% .1%; + margin: 0.5% 0.1%; height: -webkit-fill-available; user-select: none; } @@ -31,11 +31,11 @@ img { #slidePreview { flex-basis: 75%; background: rgb(43, 45, 48); - padding-right: .2%; + padding-right: 0.2%; height: 100vh; #currentSlideThumbContainer { - height: 50%; + // height: 50%; #videoToolBar { width: fit-content; @@ -43,10 +43,9 @@ img { } .currentSlideThumb { - width: auto; - height: 100%; - aspect-ratio: 16/9; - //object-fit: contain; + object-fit: contain; + height: 75%; + aspect-ratio: 16 / 9; border-radius: $slideThumbCornerRadius; } } @@ -72,15 +71,15 @@ img { width: 100%; } -div{ - #textSlidesList{ +div { + #textSlidesList { height: 22rem; } } #textSlidesList > li:hover:not(.active-text-slide) { - color: rgba(255, 255, 255, 0.50); + color: rgba(255, 255, 255, 0.5); } .active-text-slide { color: white; -} \ No newline at end of file +} diff --git a/src/renderer/js/Classes/CanvasRendererClass.js b/src/renderer/js/Classes/CanvasRendererClass.js new file mode 100644 index 0000000..a9d08a7 --- /dev/null +++ b/src/renderer/js/Classes/CanvasRendererClass.js @@ -0,0 +1,343 @@ +import Slide from "./Slide"; +import Konva from "konva"; +import addVideoSvg from "../../asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"; + +// TODO: https://chatgpt.com/share/6889bfdd-c3b0-8002-bf5b-9b05270064f0 +// eslint-disable-next-line import/no-webpack-loader-syntax +import videojs from "video.js"; +import Utils from "./Utils"; + +/** + * The Canvas renderer props + * @typedef {Object} presenter_Props + * @property {string} container - slide preview canvas id. + * @property {HTMLDivElement} sidebarSlidesContainer - sidebar container id. + * @property {HTMLDivElement} slideTextEditor - sidebar container id. + * @property {Slide[]} slides - Array of Slide objects. + * @property {number} width - Canvas width. + * @property {number} height - Canvas height. + * @property {string} basePath - The path of the base file and videos. + * @property {string} mode - Separate by words or by delimiter. + * @property {(number|string)} sepBy - Indicates whether the Wisdom component is present. + */ + +class CanvasRenderer extends Konva.Stage { + /** + * @type {[Slide]} + */ + #slides = []; + /** + * @type {number} + * */ + #currentSlide; + + #sideBarSlidesContainer; + #baseLayer; + #textLayer; + #background; + #textBackground; + #videoObj; + #anim; + #filePicker; + #controller; + #textToHeightRatio = 0.125; + + #simpleText; + #padding; + + #createFilePicker() { + let filePicker = document.createElement("input"); + filePicker.type = "file"; + filePicker.accept = "video/*"; + filePicker.addEventListener("change", this.#onVideoFilePicked.bind(this), { + signal: this.#controller.signal, + }); + return filePicker; + } + #createVideoElement() { + let videoElement = document.createElement("video"); + videoElement.autoplay = true; + videoElement.loop = true; + videoElement.controls = true; + videoElement.muted = true; + return videoElement; + } + + pickVideoFile() { + this.#filePicker.click(); + } + + #onImageLayerClicked(e) { + if (this.#currentSlide.videoFileName === undefined) { + this.pickVideoFile(); + } + if (this.#videoObj.paused) { + this.#videoObj.play(); + } else { + this.#videoObj.pause(); + } + } + + async #onVideoFilePicked(e) { + let filePicker = e.target; + let file = filePicker.files.item(0); + let videoFileName = file.name; + let imgFileName = videoFileName.replace(/\.[^/.]+$/, ""); + + try { + let generatedImage = await Utils.createVideoCoverImage(file.path); + const add = await slideFiles.addSlideFiles({ + imgBase64: generatedImage, + videoFilePath: file.path, + imgFileName, + videoFileName, + }); + } catch (err) { + console.error(err); + } + + this.onVideoPicked?.(videoFileName); + } + + /** + * Sets the canvas background to a video if the provided slide contains a video file. + * Updates the video source, sets the background image, and starts video playback and animation. + * If no video file is present in the slide, triggers the canvas video picker. + * + * @private + * @param {Slide} [slide=this.#currentSlide] - The slide object to use for setting the video background. + */ + #setCanvasToVideo(/** Slide*/ slide = this.#currentSlide) { + if (slide.videoFileName !== undefined) { + this.container().style.background = "transparent"; + // this.#videoObj.src = "file://" + this.#basePath + "/" + slide.videoFileName + "." + slide.videoFileFormat + this.#videoObj.src = + "media://" + + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat); + this.#videoObj.muted = slide.isMuted; + + this.#background.setAttrs({ + image: this.#videoObj, + x: 0, + y: 0, + width: this.width(), + height: this.height(), + }); + + this.#videoObj.play(); + this.#anim.start(); + } else { + this.#createCanvasVideoPicker(); + } + } + + #createCanvasVideoPicker() { + let imgdim = this.height() * 0.5; + Konva.Image.fromURL(addVideoSvg, (imageNode) => { + this.#background.setAttrs({ + image: imageNode.image(), + width: imgdim, + height: imgdim, + x: (this.width() - imgdim) / 2, + y: (this.height() - imgdim) / 2, + }); + }); + this.container().style.background = "#000"; + } + + constructor(props) { + super(props); + + this.container().children[0].classList.add("border"); + this.container().children[0].classList.add("border-light-subtle"); + this.#padding = 0.2; + + this.onVideoPicked = props.onVideoPicked; + this.muteVideoBtn = props.muteVideoBtn; + this.onTextDrag = props.onTextDragFn; + this.#controller = new AbortController(); + + this.#videoObj = this.#createVideoElement(); + this.#filePicker = this.#createFilePicker(); + + this.#baseLayer = new Konva.Layer(); + this.add(this.#baseLayer); + + this.#anim = new Konva.Animation(function () { + // do nothing, animation just need to update the layer + }, this.#baseLayer); + + this.#anim.start(); + + /** + * @type {Konva.Image} + */ + this.#background = new Konva.Image({ + x: 0, + y: 0, + width: this.width(), + height: this.height(), + }); + + /* this.#textLayer = new Konva.Group({ + draggable: true, + boundBoxFunc: (oldBox, newBox) => { + // Adopted from https://konvajs.org/docs/sandbox/Limited_Drag_And_Resize.html + // Calculate the actual bounding box of the transformed shape + const box = Utils.getClientRect(newBox); + + // Check if the new box is outside the stage boundaries + const isOut = + box.x < 0 || + box.y < 0 || + box.x + box.width > this.width() || + box.y + box.height > this.height(); + + // If outside boundaries, keep the old box + if (isOut) { + return oldBox; + } + + // If within boundaries, allow the transformation + return newBox; + }, + }); + */ + this.#simpleText = new Konva.Text({ + x: 0, + y: 0, + width: this.width() * 0.5, + text: "", + /* + \u200f The right-to-left mark (RLM) is a non-printing character used in the computerized typesetting of bi-directional + text containing a mix of left-to-right scripts (such as Latin and Cyrillic) and right-to-left scripts + (such as Arabic, Syriac, and Hebrew). + https://en.wikipedia.org/wiki/Right-to-left_mark + https://github.com/konvajs/konva/issues/552 + */ + fontFamily: "Calibri", + fill: "white", + id: "text", + fontSize: this.height() * this.#textToHeightRatio, + align: "center", + fontStyle: "bold", + lineHeight: 1.25, + padding: this.#padding * 10, + }); + + this.#textLayer = new Konva.Label({ + x: 0, + y: 0, + opacity: 1, + draggable: true, + }); + + this.#textBackground = new Konva.Tag({ + fill: "black", + opacity: 0.5, + cornerRadius: 12, + padding: this.#padding * 10, + }); + this.#textLayer.add(this.#textBackground); + + this.#textLayer.add(this.#simpleText); + + this.#baseLayer.add(this.#background); + + this.#baseLayer.add(this.#textLayer); + + console.log(`${this.constructor.name} initialized `); + } + + _attachEventListeners() { + this.#baseLayer.on("mouseover", function (evt) { + var shape = evt.target; + document.body.style.cursor = "pointer"; + }); + this.#baseLayer.on("mouseout", function (evt) { + var shape = evt.target; + document.body.style.cursor = "default"; + }); + + this.#baseLayer.on("click", this.#onImageLayerClicked.bind(this)); + + this.#textLayer.dragBoundFunc((pos) => { + // Clone the group and simulate the new position + const clone = this.#textLayer.clone(); + clone.position(pos); + const box = clone.getClientRect(); + + let newX = pos.x; + let newY = pos.y; + + const stageWidth = this.width(); + const stageHeight = this.height(); + + if (box.x < 0) { + newX = pos.x - box.x; + } + if (box.y < 0) { + newY = pos.y - box.y; + } + if (box.x + box.width > stageWidth) { + newX = pos.x - (box.x + box.width - stageWidth); + } + if (box.y + box.height > stageHeight) { + newY = pos.y - (box.y + box.height - stageHeight); + } + return { x: newX, y: newY }; + }); + this.#textLayer.on("dragend", (e) => { + const lastTextPos = e.target._lastPos; + const relativeTextPos = { + x: lastTextPos.x / this.width(), + y: lastTextPos.y / this.height(), + }; + this.onTextDrag?.(relativeTextPos); + }); + } + + renderSlide(slide) { + this.#currentSlide = slide; + this.#setCanvasToVideo(slide); + } + + changeVideoMuteState(isMuted) { + this.#videoObj.muted = isMuted; + } + + rendertext(text) { + this.#simpleText.text(text); + } + renderTextPosition({ x, y }) { + this.#textLayer.x(x * this.width()); + this.#textLayer.y(y * this.height()); + } + renderTextProps(textProps) { + this.#simpleText.setAttrs({ ...textProps }); + } + renderTextBackground(props) { + if (props) { + this.#textBackground.fill(props.color); + this.#textBackground.opacity(props.opacity); + } else { + this.#textBackground.opacity(0); + } + } + + destroyCreator() { + this.#videoObj.pause(); + this.#videoObj.src = ""; + this.#videoObj.load(); + while ( + this.#sideBarSlidesContainer.firstChild && + this.#sideBarSlidesContainer.removeChild( + this.#sideBarSlidesContainer.firstChild + ) + ); + this.#controller.abort(); + this.destroy(); + } +} + +export default CanvasRenderer; diff --git a/src/renderer/js/Classes/LyricManagerClass.js b/src/renderer/js/Classes/LyricManagerClass.js new file mode 100644 index 0000000..e48ab7f --- /dev/null +++ b/src/renderer/js/Classes/LyricManagerClass.js @@ -0,0 +1,58 @@ +export default class LyricManager { + static STRAT_DELIMETER = "delim"; + static STRAT_WORDS = "words"; + constructor(props) { + this.onFinished = props.onFinishedSlideCallback; + this.onPrevious = props.onPreviousSlideCallback; + this.splitStrategy = props.splitStrategy; + this.splitDelimiter = props.splitDelimiter; + this.currentSlide = null; + this.lyricChunks = []; + this.currentIndex = 0; + } + loadSlide(slide) { + this.currentSlide = slide; + this.lyricChunks = this.splitIntoChunks(slide.text); + this.currentIndex = 0; + } + + splitIntoChunks(text) { + let subtitledText = []; + + if (this.splitStrategy === LyricManager.STRAT_WORDS) { + this.splitDelimiter *= 1; + let textSplit = text.split(" "); + for (let i = 0; i < textSplit.length; i += this.splitDelimiter) { + subtitledText.push( + textSplit.slice(i, i + this.splitDelimiter).join(" ") + ); + } + } else { + subtitledText = text.split(this.splitDelimiter); + } + console.log(subtitledText); + return subtitledText; + } + getCurrentLyric() { + return this.lyricChunks[this.currentIndex] || ""; + } + + next() { + if (this.currentIndex < this.lyricChunks.length - 1) { + this.currentIndex++; + } else { + this.onFinished?.(); // Notify ShowCreator to go to next slide + } + } + + previous() { + if (this.currentIndex === 0) { + this.onPrevious?.(); + } else { + this.currentIndex--; + } + } + reset() { + this.currentIndex = 0; + } +} diff --git a/src/renderer/js/Classes/ShowClass.js b/src/renderer/js/Classes/ShowClass.js index 9274731..38362ee 100644 --- a/src/renderer/js/Classes/ShowClass.js +++ b/src/renderer/js/Classes/ShowClass.js @@ -13,76 +13,73 @@ import ShowPresentationBase from "./ShowPresentationBaseClass"; import Konva from "konva"; - /** * A class for creating presentation * @class Show * @extends ShowPresentationBase */ class Show extends ShowPresentationBase { - - /** - * Creates Video Object for a slide - * @param {Slide} slide - Slide to create its video Element - * @return {HTMLElement} - */ - _createBackground(slide) { - let VideoObj = document.createElement('video'); - VideoObj.src = "media://" + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat) - // VideoObj.src = `${this._basePath}/${slide.videoFileName}.${slide.videoFileFormat}` - VideoObj.muted = true - VideoObj.loop = true - VideoObj.preload = "auto" - return VideoObj + /** + * Creates Video Object for a slide + * @param {Slide} slide - Slide to create its video Element + * @return {HTMLElement} + */ + _createBackground(slide) { + let VideoObj = document.createElement("video"); + VideoObj.src = + "media://" + + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat); + // VideoObj.src = `${this._basePath}/${slide.videoFileName}.${slide.videoFileFormat}` + VideoObj.muted = slide.isMuted; + VideoObj.loop = true; + VideoObj.preload = "auto"; + return VideoObj; + } + + /** + * Create a Presentation. + * @param {presentation_Props} props - presentation props + */ + + constructor(props) { + super(props); + + this._backgroundObjs[0].play(); + + this.anim = new Konva.Animation(function () { + // do nothing, animation just need to update the layer + }, this.baseLayer); + + this.anim.start(); + } + + destroyShow() { + this._backgroundObjs.forEach((element, index) => { + element.pause(); + element.src = ""; + element.load(); + }); + } + + /** + * Changes the slides dynamically the lyrics first then the video + * @param number + * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} + */ + changeSlide(number = 1) { + let { currentTextSlide, currentSlide, isNewSlide } = super.changeSlide( + number + ); + if (isNewSlide) { + if (this.slides[currentSlide].videoFileName !== undefined) { + this._backgroundObjs[currentSlide].play(); + } + if (currentSlide !== this._slides.length - 1) { + this._backgroundObjs[currentSlide + number].pause(); + } } - - - /** - * Create a Presentation. - * @param {presentation_Props} props - presentation props - */ - - constructor(props) { - super(props) - - this._backgroundObjs[0].play() - - - this.anim = new Konva.Animation(function () { - // do nothing, animation just need to update the layer - }, this.baseLayer); - - this.anim.start() - } - - destroyShow() { - this._backgroundObjs.forEach((element, index) => { - element.pause() - element.src = '' - element.load() - - }) - } - - /** - * Changes the slides dynamically the lyrics first then the video - * @param number - * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} - */ - changeSlide(number = 1) { - let {currentTextSlide, currentSlide, isNewSlide} = super.changeSlide(number) - if (isNewSlide) { - if (this.slides[currentSlide].videoFileName !== undefined) { - this._backgroundObjs[currentSlide].play() - } - if (currentSlide !== this._slides.length - 1) { - this._backgroundObjs[currentSlide + number].pause() - } - } - return {currentTextSlide, currentSlide, isNewSlide} - } - - + return { currentTextSlide, currentSlide, isNewSlide }; + } } -export default Show; \ No newline at end of file +export default Show; diff --git a/src/renderer/js/Classes/ShowCreator.js b/src/renderer/js/Classes/ShowCreator.js new file mode 100644 index 0000000..ea453be --- /dev/null +++ b/src/renderer/js/Classes/ShowCreator.js @@ -0,0 +1,190 @@ +import CanvasRenderer from "./CanvasRendererClass"; +import SidebarRenderer from "./SidebarRendererClass"; +import SlideManager from "./SlideManager"; +import VideoToolbar from "./VideoToolbarClass"; +import Slide from "./Slide"; +import LyricManager from "./LyricManagerClass"; +import TextEditorArea from "./TextEditorClass"; + +/** + * Manages the creation and editing of a video slideshow, including slide management, + * sidebar rendering, canvas rendering, and event handling for UI controls. + * + * @class + * @classdesc Handles the main logic for adding, removing, and updating slides, + * as well as synchronizing UI components such as the sidebar and canvas. + * + */ +class ShowCreator { + #addSlideBtn; + #removeSlideBtn; + #textEditorField; + /** + * Creates an instance of ShowCreator. + * @param {Object} props - Configuration properties for ShowCreator. + * @param {Slide[]} props.slides - Initial slides to load. + * @param {HTMLElement} props.sidebarSlidesContainer - Container for sidebar slides. + * @param {string} props.container - Canvas container element. + * @param {number} props.width - Width of the canvas. + * @param {number} props.height - Height of the canvas. + * @param {HTMLElement} props.addSlideBtn - Button to add a new slide. + * @param {HTMLElement} props.removeSlideBtn - Button to remove the current slide. + * @param {HTMLTextAreaElement} props.textEditorField - Text area for editing slide text. + */ + constructor(props) { + this.slides = new SlideManager({ + slides: props.slides, + onSlideChange: this.onSlideChange.bind(this), + }); + this.lyrics = new LyricManager({ + onFinished: () => this.onLyricsSlideFinished(), + onPrevious: () => this.onLyricsSlidePrevious(), + splitStrategy: props.splitStrategy, + splitDelimiter: props.splitDelimiter, + }); + this.sidebar = new SidebarRenderer({ + container: props.sidebarSlidesContainer, + onSlideClickfn: this.onSlideClicked.bind(this), + }); + this.canvas = new CanvasRenderer({ + container: props.container, + width: (props.height * 16) / 9, + height: props.height, + onVideoPicked: this.onVideoPicked.bind(this), + onTextDragFn: this.onTextDrag.bind(this), + }); + + this.videoToolbar = new VideoToolbar({ + container: props.videoToolbar, + onMuteButton: () => this.onMuteButtonClicked(), + }); + this.textEditor = new TextEditorArea({ + textAreaElement: props.textEditorField, + fontSelectorElement: props.fontSelector, + backgroundBtnElemnt: props.backgroundEnabledBtn, + onTextEditedFn: this.onTextEdited.bind(this), + onFontSelectedFn: this.onFontSelected.bind(this), + onBackgroundToggle: this.onBackgroundBtn.bind(this), + }); + + this.#addSlideBtn = props.addSlideBtn; + this.#removeSlideBtn = props.removeSlideBtn; + this.#textEditorField = props.textEditorField; + this.#renderInitialSlides(); + this.#attachEventListeners(); + console.log(`${this.constructor.name} initialized `); + } + + #attachEventListeners() { + // hook into addSlide, removeSlide, textarea input, etc. + this.#addSlideBtn.addEventListener("click", (e) => this.addNewSlide()); + this.#removeSlideBtn.addEventListener("click", (e) => this.removeSlide()); + + this.sidebar._attachEventListeners(); + this.canvas._attachEventListeners(); + this.videoToolbar._attachEventListeners(); + this.textEditor._attachEventListeners(); + } + + onSlideChange() { + console.log(this.slides.currentSlide); + this.canvas.renderTextPosition(this.slides.currentSlide.textPosition); + this.canvas.renderSlide(this.slides.currentSlide); + this.lyrics.loadSlide(this.slides.currentSlide); + this.canvas.rendertext(this.lyrics.getCurrentLyric()); + this.canvas.renderTextProps({ + fontFamily: this.slides.currentSlide.fontFamily, + }); + + this.textEditor.setTextArea(this.slides.currentSlide.text); + this.textEditor.setFontSelector(this.slides.currentSlide.fontFamily); + + this.canvas.renderTextBackground(this.slides.currentSlide.fontBackground); + this.textEditor.renderBackgroundBtn( + !!this.slides.currentSlide.fontBackground.color + ); + + this.videoToolbar.changeMuteButtonIcon(this.slides.currentSlide.isMuted); + } + + onLyricsSlideFinished() { + this.slides.setCurrent(this.slides.currentIndex + 1); + } + onLyricsSlidePrevious() { + this.slides.setCurrent(this.slides.currentIndex - 1); + } + + addNewSlide( + slide = new Slide({ + text: { value: `Slide ${this.slides.allSlides.length + 1}` }, + video: { name: undefined, muted: true }, + }) + ) { + const idx = this.slides.addSlide(slide); + this.sidebar.addSlideElement(slide, idx); + } + + removeSlide() { + const idx = this.slides.removeSlide(); + this.sidebar.removeSlideElement(idx, this.slides.currentIndex); + // this.#textEditorField.value = this.slides.currentSlide.text; + // this.canvas.renderSlide(this.slides.currentSlide); + } + + #renderInitialSlides() { + this.sidebar.clear(); + if (this.slides.allSlides.length === 0) { + this.addNewSlide(); + } else { + this.slides.allSlides.forEach((s, idx) => { + this.sidebar.addSlideElement(s, idx); + + if (idx === this.slides.currentIndex) { + this.onSlideChange(); + } + }); + } + } + + onTextDrag({ x, y }) { + this.slides.updateTextPosition(x, y); + } + onTextEdited(text) { + this.slides.updateSlideText(text); + this.lyrics.loadSlide(this.slides.currentSlide); + this.canvas.rendertext(this.lyrics.getCurrentLyric()); + this.sidebar.rerenderSlideElementText(this.slides.currentIndex, text); + } + + onVideoPicked(filename) { + this.slides.updateSlideVideo(filename); + console.log(this.slides.currentSlide); + this.sidebar.rerenderSlideThumbnail( + this.slides.currentIndex, + this.slides.currentSlide + ); + this.canvas.renderSlide(this.slides.currentSlide); + } + + onSlideClicked(slideNumber) { + this.slides.setCurrent(slideNumber); + } + + onMuteButtonClicked() { + let newMuteState = this.slides.toggleMuteSlide(); + this.videoToolbar.changeMuteButtonIcon(newMuteState); + this.canvas.changeVideoMuteState(newMuteState); + } + onFontSelected(font) { + this.slides.updateTextFont(font); + this.canvas.renderTextProps({ fontFamily: font }); + } + onBackgroundBtn() { + let backgroundProps = this.slides.toggleSlideBackground(); + this.canvas.renderTextBackground(backgroundProps); + } + stringifyShow() { + return JSON.stringify(this.slides); + } +} +export default ShowCreator; diff --git a/src/renderer/js/Classes/ShowCreatorClass.js b/src/renderer/js/Classes/ShowCreatorClass.js deleted file mode 100644 index 2769588..0000000 --- a/src/renderer/js/Classes/ShowCreatorClass.js +++ /dev/null @@ -1,340 +0,0 @@ -import Slide from "./SlideClass"; -import Konva from "konva"; -import addVideoSvg from '../../asset/resource/video-add-svgrepo-com.svg' - - -/** - * The presenter view props - * @typedef {Object} presenter_Props - * @property {string} container - slide preview canvas id. - * @property {HTMLDivElement} sidebarSlidesContainer - sidebar container id. - * @property {HTMLDivElement} slideTextEditor - sidebar container id. - * @property {Slide[]} slides - Array of Slide objects. - * @property {number} width - Canvas width. - * @property {number} height - Canvas height. - * @property {string} basePath - The path of the base file and videos. - * @property {string} mode - Separate by words or by delimiter. - * @property {(number|string)} sepBy - Indicates whether the Wisdom component is present. - */ - -class ShowCreator extends Konva.Stage { - - /** - * @type {[Slide]} - */ - #slides = [] - /** - * @type {number} - * */ - #currentSlide = -1; - #w; - #h; - #basePath; - #sideBarSlidesContainer; - #addSlideBtn; - #baseLayer; - #background; - #slideTextEditor; - #slideTextInput; - #videoObj; - #anim; - #filePicker; - #controller; - #unrenderedSlides = []; - - - #slidesRadioSelector() { - return document.querySelectorAll(`#${this.#sideBarSlidesContainer.id} input`) - }; - - /** - * Creates sidebar preview Object for a slide - * @param {Slide} slide - Slide to create its image Element - * @param {number} slideId Represent slide id in the list - */ - #createSlideSidebarPreview(slide, slideId) { - let slideContainer = document.createElement("li") - - let radioBtn = document.createElement("input"); - radioBtn.setAttribute("type", "radio"); - radioBtn.name = "slides" - radioBtn.id = "s" + slideId; - - slideContainer.appendChild(radioBtn) - - slideContainer.className = "list-group-item list-group-item-action d-flex" - - let LabelContainer = document.createElement("label"); - // LabelContainer.className = "list-group-item"; - LabelContainer.setAttribute('for', 's' + slideId) - - let divInLabel = document.createElement("div"); - divInLabel.className = "slideThumbContainer "; - - - let slideThumbPreview = document.createElement("img"); - slideThumbPreview.className = "slideThumb"; - if (slide.videoFileName === undefined) { - slide.createVideoCoverImage().then(img => { - slideThumbPreview.src = `${img}`; - }) - } else { - slideThumbPreview.src = "file://" + this.#basePath + "/" + slide.videoFileName + "." + slide.videoThumbnailFormat; - } - - - let slidePrevTextContainer = document.createElement("span"); - slidePrevTextContainer.className = "slideText"; - slidePrevTextContainer.innerText = slide.text - slidePrevTextContainer.dataset.slidenumber = slideId; - slidePrevTextContainer.dir = "rtl" - divInLabel.appendChild(slideThumbPreview); - divInLabel.appendChild(slidePrevTextContainer); - LabelContainer.appendChild(divInLabel); - slideContainer.appendChild(LabelContainer); - - - return slideContainer; - } - - #findSelectedSlidePos() { - return [...this.#slidesRadioSelector()].findIndex(el => el.checked) - } - - #onChangeSlideText(e) { - - // change slide text values and sidebar - let currentText = e.target.value.trim() - this.#slides[this.#currentSlide].text = currentText; - this.#sideBarSlidesContainer.children[this.#currentSlide].querySelector("span").innerText = currentText - } - - #onSlideClick(e) { - /** - * https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_delegation - */ - - this.#currentSlide = this.#findSelectedSlidePos() - this.#slideTextInput.value = this.#slides[this.#currentSlide].text - this.#videoObj.src = "file://" + this.#basePath + "/" + this.#slides[this.#currentSlide].videoFileName + "." + this.#slides[this.#currentSlide].videoFileFormat - this.#setCanvasToVideo() - console.log(this.#currentSlide, this.#slides.length); - - } - - #createFilePicker() { - let filePicker = document.createElement("input"); - filePicker.type = "file"; - filePicker.accept = "video/*"; - filePicker.addEventListener("change", this.#onVideoFilePicked.bind(this), {signal: this.#controller.signal}); - return filePicker - } - - #onImageLayerClicked(e) { - if (this.#slides[this.#currentSlide].videoFileName === undefined) { - this.#filePicker.click() - } - if (this.#videoObj.paused) { - this.#videoObj.play(); - } else { - this.#videoObj.pause(); - } - } - - async #onVideoFilePicked(e) { - - let filePicker = e.target - console.log(filePicker.files.item(0)) - let file = filePicker.files.item(0) - let fileName = file.name - console.log(this, fileName) - this.#slides[this.#currentSlide].videoFileName = fileName; - let videoBasePath = file.path.split("\\") - videoBasePath.pop() - videoBasePath = videoBasePath.join("\\") - let currentSlideImg = this.#slides[this.#currentSlide] - let generatedImage = await currentSlideImg.createVideoCoverImage(videoBasePath) - await thumbs.create({pic: generatedImage, filename: currentSlideImg.videoFileName}) - await window.file.copyVideo(file.path) - this.#sideBarSlidesContainer.children[this.#currentSlide].querySelector("img").src = "file://" + this.#basePath + "/" + currentSlideImg.videoFileName + "." + currentSlideImg.videoThumbnailFormat - this.#setCanvasToVideo(currentSlideImg) - } - - #setCanvasToVideo(/** Slide*/slide = this.#slides[this.#currentSlide]) { - if (slide.videoFileName !== undefined) { - this.container().style.background = "transparent" - // this.#videoObj.src = "file://" + this.#basePath + "/" + slide.videoFileName + "." + slide.videoFileFormat - this.#videoObj.src = "media://" + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat) - console.log(this.#videoObj) - this.#background.setAttrs({ - image: this.#videoObj, - x: 0, - y: 0, - width: this.width(), - height: this.height(), - }); - - this.#videoObj.play() - this.#anim.start() - - this.#slideTextInput.value = slide.text - } else { - this.#createCanvasVideoPicker() - } - } - - #createCanvasVideoPicker() { - let imgdim = this.height() * .50 - Konva.Image.fromURL(addVideoSvg, (imageNode) => { - this.#background.setAttrs({ - image: imageNode.image(), - width: imgdim, - height: imgdim, - x: (this.width() - imgdim) / 2, - y: (this.height() - imgdim) / 2 - }); - - }); - this.container().style.background = "#000" - } - - constructor(props) { - super(props); - this.#sideBarSlidesContainer = props.sidebarSlidesContainer - this.#slideTextEditor = props.slideTextEditor - this.#slideTextInput = this.#slideTextEditor.querySelector(`textarea`); - this.#addSlideBtn = document.querySelector(`#slideAdd`); - - this.#controller = new AbortController(); - - - this.#basePath = props.basePath - this.#unrenderedSlides = props.slides - - console.log(this.#slides) - - this.#videoObj = document.createElement("video"); - this.#videoObj.autoplay = true; - this.#videoObj.loop = true - this.#videoObj.muted = true - - - this.#filePicker = this.#createFilePicker() - - - this.#baseLayer = new Konva.Layer({}); - this.add(this.#baseLayer) - - - this.#addSlideBtn.addEventListener("click", () => { - this.addNewSlide() - }, {signal: this.#controller.signal}) - this.#sideBarSlidesContainer.addEventListener("change", this.#onSlideClick.bind(this), {signal: this.#controller.signal}) - this.#slideTextInput.addEventListener("input", this.#onChangeSlideText.bind(this), {signal: this.#controller.signal}) - - this.#anim = new Konva.Animation(function () { - // do nothing, animation just need to update the layer - }, this.#baseLayer); - - this.#anim.start() - - this.#baseLayer.on('mouseover', function (evt) { - var shape = evt.target; - document.body.style.cursor = 'pointer'; - - }); - this.#baseLayer.on('mouseout', function (evt) { - var shape = evt.target; - document.body.style.cursor = 'default'; - }); - - this.#baseLayer.on('click', this.#onImageLayerClicked.bind(this)) - - /** - * - * @type {Konva.Image} - */ - this.#background = new Konva.Image({ - x: 0, y: 0, width: this.width(), height: this.height(), - }); - - this.#baseLayer.add(this.#background) - - this.slideNumber = 0 - - // mapping slides must be at end - this.#unrenderedSlides.forEach(slide => this.addNewSlide(slide)) - - console.log(this.#slides) - if (this.#slides.length === 0) { - this.addNewSlide() - } - } - - - /** - * Adds New slide to the show - * - */ - addNewSlide(newSlide = new Slide({ - text: this.slideNumber + " Please add slide text", videoFileName: undefined - })) { - - - let itemToInsertBefore = this.#sideBarSlidesContainer.children[this.#currentSlide + 1] - this.#currentSlide += 1 - this.#sideBarSlidesContainer.insertBefore(this.#createSlideSidebarPreview(newSlide, ++this.slideNumber), itemToInsertBefore) - document.querySelectorAll("#sidebarSlidesContainer input")[this.#currentSlide].checked = true - - let scrollBehaviour = {behavior: "smooth", block: "start"} - this.#sideBarSlidesContainer.children[this.#currentSlide].scrollIntoView(scrollBehaviour) - this.#slides.splice(this.#currentSlide, 0, newSlide) - - - if (!newSlide.videoFileName) { - this.#slideTextInput.value = "" - this.#createCanvasVideoPicker() - } else { - this.#setCanvasToVideo(this.#slides[this.#currentSlide]) - } - - console.log(this.#currentSlide,this.#slides) - } - - - /** - * Removes slide from the show - * - */ - removeSlide() { - if (this.#currentSlide > -1) { - this.#slides.splice(this.#currentSlide, 1) - let slideElementToRemove = this.#sideBarSlidesContainer.children[this.#currentSlide] - this.#sideBarSlidesContainer.removeChild(slideElementToRemove) - this.#currentSlide -= 1 - - // console.log(this.#currentSlide, this.#slides.length) - if (this.#currentSlide > -1) { - this.#slidesRadioSelector()[this.#currentSlide].checked = true - } - this.#setCanvasToVideo(this.#slides[this.#currentSlide]) - - //TODO: Delete video and Thumbnail from folder - } - } - - saveShow() { - return JSON.stringify(this.#slides) - } - - destroyCreator() { - this.#videoObj.pause() - this.#videoObj.src = '' - this.#videoObj.load() - while (this.#sideBarSlidesContainer.firstChild && this.#sideBarSlidesContainer.removeChild(this.#sideBarSlidesContainer.firstChild)) ; - this.#controller.abort() - this.destroy() - } -} - -export default ShowCreator; \ No newline at end of file diff --git a/src/renderer/js/Classes/ShowPresentationBaseClass.js b/src/renderer/js/Classes/ShowPresentationBaseClass.js index 5c4707e..a0706e1 100644 --- a/src/renderer/js/Classes/ShowPresentationBaseClass.js +++ b/src/renderer/js/Classes/ShowPresentationBaseClass.js @@ -1,192 +1,233 @@ import Konva from "konva"; class ShowPresentationBase extends Konva.Stage { - #w; - #h; - #textToHeightRatio = 0.125 - #textToSpacingRatio = .65 - _basePath; - #simpleText; - #background; - #textSlides; - #currentSlide - #currentTextSlide; - _slides - #textLayer; - #textBG; - #padding = 20; - _backgroundObjs; - _mode; - _sepBy; - static WORDS = "words"; - static DELIMITER = "delimiter"; - - get slides() { - return this._slides; - } - - get basePath() { - return this._basePath; - } - - get mode() { - return this._mode; - } - - get sepBy() { - return this._sepBy; - } - - /** - * Creates Video Object for a slide - * @param {Slide} slide - Slide to create its video Element - * @return {HTMLElement} - */ - _createBackground(slide) { - return new Image(); - } - - /** - * Create video objects for caching. - * @param {number} numOfVideos - Number of videos to be cached after the current video - */ - _cacheBG(numOfVideos = 3) { - return this._slides.slice(this.#currentSlide + 2, this.#currentSlide + 2 + numOfVideos).map(this._createBackground.bind(this)) - } - - /** - * Create a Presentation. - * @param {presentation_Props} props - presentation props - */ - constructor(props) { - super(props); - this._slides = props.slides - this.#w = props.width - this.#h = props.height - this._basePath = props.basePath - this._mode = props.mode - this._sepBy = props.sepBy - this.#currentSlide = 0 - this.#currentTextSlide = 0 - this.#textSlides = this._slides[this.#currentSlide].splitText( - this._mode, - this._sepBy - ) - this._backgroundObjs = this._slides.slice(0, 3).map(this._createBackground.bind(this)) - console.log(this._backgroundObjs) - - this.baseLayer = new Konva.Layer(); - this.#textLayer = new Konva.Layer(); - - this.#background = new Konva.Image({ - image: this._backgroundObjs[0], - x: 0, - y: 0, - width: this.width(), - height: this.height(), - }); - - console.log((this.height() - 225) / this.height(),) - this.#simpleText = new Konva.Text({ - x: 0, - y: this.height() * this.#textToSpacingRatio, - text: `${this.#textSlides[this.#currentTextSlide]}\u202e`, - /* - \u200f The right-to-left mark (RLM) is a non-printing character used in the computerized typesetting of bi-directional - text containing a mix of left-to-right scripts (such as Latin and Cyrillic) and right-to-left scripts - (such as Arabic, Syriac, and Hebrew). - https://en.wikipedia.org/wiki/Right-to-left_mark - https://github.com/konvajs/konva/issues/552 - */ - fontSize: this.height() * this.#textToHeightRatio, - fontFamily: 'Calibri', - fill: 'white', - id: "text", - draggable: false, - width: this.width(), - align: "center", - cornerRadius: 20, - fontStyle: "bold", - lineHeight:1.25 - }); - - this.#textBG = new Konva.Rect({ - // x: this.#w / 2 - this.#simpleText.getTextWidth() / 2 - this.#padding / 2, - x: this.#simpleText.getClientRect().x - this.#padding / 2, - y: this.#simpleText.getClientRect().y - this.#padding / 2, - height: this.#simpleText.getClientRect().height + this.#padding, - width: this.#simpleText.getTextWidth() + this.#padding, - cornerRadius: 20, - fill: "#000", - opacity: .5 - }) - - this.baseLayer.add(this.#background); - - this.#textLayer.getCanvas()._canvas.setAttribute("dir","rtl") - this.#textLayer.add(this.#textBG) - this.#textLayer.add(this.#simpleText) - - this.add(this.baseLayer); - this.add(this.#textLayer) - - this.baseLayer.draw(); - } - - /** - * Changes the slides dynamically the lyrics first then the video - * @param number - * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} - */ - changeSlide(number = 1) { - this.#currentTextSlide += number - let isNewSlide = false - if (this.#currentTextSlide < this.#textSlides.length && this.#currentTextSlide >= 0) { - this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`) - } else if (this.#currentTextSlide === this.#textSlides.length && this.#currentSlide !== this._slides.length - 1) { - this.#currentTextSlide = 0 - this.#currentSlide++ - this.#textSlides = this._slides[this.#currentSlide].splitText( - this._mode, - this._sepBy - ) - this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}\u202e`) - isNewSlide = true - - if (this.#currentSlide === this._backgroundObjs.length - 2) { - this._backgroundObjs.push(...this._cacheBG()) - console.log(this._backgroundObjs) - } - this.#background.setAttr("image", this._backgroundObjs[this.#currentSlide]) - } else if (this.#currentTextSlide < 0 && this.#currentSlide !== 0) { - this.#currentSlide-- - this.#textSlides = this._slides[this.#currentSlide].splitText( - this._mode, - this._sepBy - ) - this.#currentTextSlide = this.#textSlides.length + this.#currentTextSlide - this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`) - isNewSlide = true - this.#background.setAttr("image", this._backgroundObjs[this.#currentSlide]) - } else if (this.#currentTextSlide < 0 && this.#currentSlide === 0) { - this.#currentTextSlide = 0 - } else { - this.#currentTextSlide = this.#textSlides.length - 1 - } - - this.#textBG.setAttrs({ - x: this.#w / 2 - this.#simpleText.getTextWidth() / 2 - this.#padding / 2, - y: this.#simpleText.getClientRect().y - this.#padding / 2, - height: this.#simpleText.getClientRect().height + this.#padding, - width: this.#simpleText.getTextWidth() + this.#padding, - }) - console.log(`Text ${this.#currentTextSlide + 1} of ${this.#textSlides.length},`, - `Slide ${this.#currentSlide + 1} of ${this._slides.length},\n`, - `Text Height: ${this.#simpleText.getClientRect().y - this.#padding / 2}`, - `compare to height: ${+this.#simpleText.getClientRect().y - this.#padding / 2}`) - return {currentTextSlide: this.#currentTextSlide, currentSlide: this.#currentSlide, isNewSlide} + #w; + #h; + #textToHeightRatio = 0.125; + #textToSpacingRatio = 0.65; + _basePath; + #simpleText; + #background; + #textSlides; + #currentSlide; + #currentTextSlide; + _slides; + #textLayer; + #textBG; + #textBackground; + #padding = 0.2; + _backgroundObjs; + _mode; + _sepBy; + static WORDS = "words"; + static DELIMITER = "delimiter"; + + get slides() { + return this._slides; + } + + get basePath() { + return this._basePath; + } + + get mode() { + return this._mode; + } + + get sepBy() { + return this._sepBy; + } + + /** + * Creates Video Object for a slide + * @param {Slide} slide - Slide to create its video Element + * @return {HTMLElement} + */ + _createBackground(slide) { + return new Image(); + } + + /** + * Create video objects for caching. + * @param {number} numOfVideos - Number of videos to be cached after the current video + */ + _cacheBG(numOfVideos = 3) { + return this._slides + .slice(this.#currentSlide + 2, this.#currentSlide + 2 + numOfVideos) + .map(this._createBackground.bind(this)); + } + + /** + * Create a Presentation. + * @param {presentation_Props} props - presentation props + */ + constructor(props) { + super(props); + this._slides = props.slides; + this.#w = props.width; + this.#h = props.height; + this._basePath = props.basePath; + this._mode = props.mode; + this._sepBy = props.sepBy; + this.#currentSlide = 0; + this.#currentTextSlide = 0; + this.#textSlides = this._slides[this.#currentSlide].splitText( + this._mode, + this._sepBy + ); + this._backgroundObjs = this._slides + .slice(0, 3) + .map(this._createBackground.bind(this)); + console.log(this._backgroundObjs); + + this.baseLayer = new Konva.Layer(); + + this.#background = new Konva.Image({ + image: this._backgroundObjs[0], + x: 0, + y: 0, + width: this.width(), + height: this.height(), + }); + + this.#simpleText = new Konva.Text({ + x: 0, + y: 0, + width: this.width() * 0.5, + text: this.#textSlides[this.#currentTextSlide] + "\u202e", + /* + \u200f The right-to-left mark (RLM) is a non-printing character used in the computerized typesetting of bi-directional + text containing a mix of left-to-right scripts (such as Latin and Cyrillic) and right-to-left scripts + (such as Arabic, Syriac, and Hebrew). + https://en.wikipedia.org/wiki/Right-to-left_mark + https://github.com/konvajs/konva/issues/552 + */ + fontFamily: this.slides[this.#currentSlide].fontFamily, + fill: "white", + id: "text", + fontSize: this.height() * this.#textToHeightRatio, + align: "center", + fontStyle: "bold", + lineHeight: 1.25, + padding: this.#padding * 10, + }); + + this.#textLayer = new Konva.Label({ + x: 0, + y: 0, + opacity: 1, + draggable: false, + }); + + this.#textBackground = new Konva.Tag({ + fill: "black", + opacity: this.slides[this.#currentSlide].fontBackground ? 0.5 : 0, + cornerRadius: 12, + padding: this.#padding * 10, + }); + + this.#textLayer.add(this.#textBackground); + + this.#textLayer.add(this.#simpleText); + + this.baseLayer.add(this.#background); + this.baseLayer.add(this.#textLayer); + this.add(this.baseLayer); + + this.baseLayer.draw(); + } + + /** + * Changes the slides dynamically the lyrics first then the video + * @param number + * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} + */ + changeSlide(number = 1) { + this.#currentTextSlide += number; + let isNewSlide = false; + if ( + this.#currentTextSlide < this.#textSlides.length && + this.#currentTextSlide >= 0 + ) { + this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`); + } else if ( + this.#currentTextSlide === this.#textSlides.length && + this.#currentSlide !== this._slides.length - 1 + ) { + this.#currentTextSlide = 0; + this.#currentSlide++; + this.#textSlides = this._slides[this.#currentSlide].splitText( + this._mode, + this._sepBy + ); + this.#simpleText.text( + `${this.#textSlides[this.#currentTextSlide]}\u202e` + ); + this.#simpleText.fontFamily(this._slides[this.#currentSlide].fontFamily); + this.#textLayer.x( + this.width() * this._slides[this.#currentSlide].textPosition.x + ); + this.#textLayer.y( + this.height() * this._slides[this.#currentSlide].textPosition.y + ); + if (this._slides[this.#currentSlide].fontBackground) { + this.#textBackground.opacity(0.5); + this.#textBackground.fill("black"); + } else { + this.#textBackground.opacity(0); + } + isNewSlide = true; + + if (this.#currentSlide === this._backgroundObjs.length - 2) { + this._backgroundObjs.push(...this._cacheBG()); + console.log(this._backgroundObjs); + } + this.#background.setAttr( + "image", + this._backgroundObjs[this.#currentSlide] + ); + } else if (this.#currentTextSlide < 0 && this.#currentSlide !== 0) { + this.#currentSlide--; + this.#textSlides = this._slides[this.#currentSlide].splitText( + this._mode, + this._sepBy + ); + this.#currentTextSlide = this.#textSlides.length + this.#currentTextSlide; + this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`); + this.#simpleText.fontFamily(this._slides[this.#currentSlide].fontFamily); + this.#textLayer.x(this._slides[this.#currentSlide].textPosition.x); + this.#textLayer.y(this._slides[this.#currentSlide].textPosition.y); + if (this._slides[this.#currentSlide].fontBackground) { + this.#textBackground.opacity(0.5); + this.#textBackground.fill("black"); + } else { + this.#textBackground.opacity(0); + } + isNewSlide = true; + this.#background.setAttr( + "image", + this._backgroundObjs[this.#currentSlide] + ); + } else if (this.#currentTextSlide < 0 && this.#currentSlide === 0) { + this.#currentTextSlide = 0; + } else { + this.#currentTextSlide = this.#textSlides.length - 1; } + console.log( + `Text ${this.#currentTextSlide + 1} of ${this.#textSlides.length},`, + `Slide ${this.#currentSlide + 1} of ${this._slides.length},\n`, + `Text Height: ${this.#simpleText.getClientRect().y - this.#padding / 2}`, + `compare to height: ${ + +this.#simpleText.getClientRect().y - this.#padding / 2 + }` + ); + return { + currentTextSlide: this.#currentTextSlide, + currentSlide: this.#currentSlide, + isNewSlide, + }; + } } -export default ShowPresentationBase \ No newline at end of file +export default ShowPresentationBase; diff --git a/src/renderer/js/Classes/ShowPresenterViewClass.js b/src/renderer/js/Classes/ShowPresenterViewClass.js index d753cfa..35082c8 100644 --- a/src/renderer/js/Classes/ShowPresenterViewClass.js +++ b/src/renderer/js/Classes/ShowPresenterViewClass.js @@ -20,116 +20,134 @@ import ShowPresentationBase from "./ShowPresentationBaseClass"; * @extends ShowPresentationBase */ class ShowPresenterView extends ShowPresentationBase { - - #lyricsContainer; - #sideBarSlidesContainer - - /** - * Creates Image Object for a slide - * @param {Slide} slide - Slide to create its image Element - * @return {HTMLElement} - */ - _createBackground(slide) { - let ImgObj = document.createElement('img'); - // VideoObj.src = `media-loader://${encodeURIComponent(`${this.#basePath}/${slide.videoFile}`)}` - ImgObj.src = `file://${this.basePath}/${slide.videoFileName}.${slide.videoThumbnailFormat}` - return ImgObj - } - - /** - * Creates sidebar preview Object for a slide - * @param {Slide} slide - Slide to create its image Element - */ - #createSlideSidebarPreview(slide) { - let slideContainer = document.createElement("li"); - slideContainer.className = "list-group-item list-group-item-action d-box" - - let divContainer = document.createElement("div"); - divContainer.className = "slideThumbContainer"; - - let slideThumbPreview = document.createElement("img"); - slideThumbPreview.className = "slideThumb"; - slideThumbPreview.src = `file://${this.basePath}/${slide.videoFileName}.${slide.videoThumbnailFormat}`; - let slidePrevTextContainer = document.createElement("span"); - slidePrevTextContainer.className = "slideText"; - slidePrevTextContainer.innerText = slide.text - slidePrevTextContainer.dir = "rtl" - - divContainer.appendChild(slideThumbPreview); - divContainer.appendChild(slidePrevTextContainer); - - slideContainer.appendChild(divContainer); - - return slideContainer; - } - - /** - * Creates sidebar preview Object for a slide - * @param {Slide} slide - * @return {HTMLLIElement[]} - */ - #createLyricsPreview(slide) { - let lyricsLines = slide.splitText(this._mode, this._sepBy); - return lyricsLines.map((line, i) => { - //
  • ها بطيب
  • - let liEl = document.createElement("li"); - liEl.textContent = line - liEl.dataset.textslidenumber = i - liEl.dir = "rtl" - return liEl - }); - } - - /** - * Create a Presenter View. - * @param {presenter_Props} props - presentation props - */ - constructor(props) { - super(props); - this.#sideBarSlidesContainer = props.sidebarSlidesContainer - this.#lyricsContainer = props.lyricsContainer - - - let addSlideBtn = document.querySelector(`#${this.#sideBarSlidesContainer.id}>:first-child`); - - this._slides.forEach(slide => { - this.#sideBarSlidesContainer.insertBefore(this.#createSlideSidebarPreview(slide), addSlideBtn) - }) - - this.#lyricsContainer.replaceChildren(...this.#createLyricsPreview(this._slides[0])) - - this.#sideBarSlidesContainer.children[0].classList.add("active"); - this.#lyricsContainer.children[0].classList.add("active-text-slide"); - } - - /** - * Changes the slides dynamically the lyrics first then the video - * @param number - * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} - */ - - changeSlide(number = 1) { - let scrollBehaviour = {behavior: "smooth", block: "center"} - let {currentSlide, currentTextSlide, isNewSlide} = super.changeSlide(number); - - if (isNewSlide) { - let currentActive = this.#sideBarSlidesContainer.children[currentSlide] - this.#lyricsContainer.replaceChildren(...this.#createLyricsPreview(this._slides[currentSlide])) - this.#lyricsContainer.children[currentTextSlide].classList.add("active-text-slide"); - currentActive.classList.add("active"); - currentActive.scrollIntoView(scrollBehaviour) - this.#sideBarSlidesContainer.children[currentSlide - number].classList.remove("active"); - } else { - this.#lyricsContainer.children[currentTextSlide].classList.add("active-text-slide"); - this.#lyricsContainer.children[currentTextSlide].scrollIntoView(scrollBehaviour) - if (this.#lyricsContainer.children.length > 1) { - this.#lyricsContainer.children[currentTextSlide - number].classList.remove("active-text-slide"); - } - } - - return {currentSlide, currentTextSlide, isNewSlide} + #lyricsContainer; + #sideBarSlidesContainer; + + /** + * Creates Image Object for a slide + * @param {Slide} slide - Slide to create its image Element + * @return {HTMLElement} + */ + _createBackground(slide) { + let ImgObj = document.createElement("img"); + // VideoObj.src = `media-loader://${encodeURIComponent(`${this.#basePath}/${slide.videoFile}`)}` + ImgObj.src = `media://${slide.videoFileName}.${slide.videoThumbnailFormat}`; + return ImgObj; + } + + /** + * Creates sidebar preview Object for a slide + * @param {Slide} slide - Slide to create its image Element + */ + #createSlideSidebarPreview(slide) { + let slideContainer = document.createElement("li"); + slideContainer.className = "list-group-item list-group-item-action d-box"; + + let divContainer = document.createElement("div"); + divContainer.className = "slideThumbContainer"; + + let slideThumbPreview = document.createElement("img"); + slideThumbPreview.className = "slideThumb"; + slideThumbPreview.src = `media://${slide.videoFileName}.${slide.videoThumbnailFormat}`; + let slidePrevTextContainer = document.createElement("span"); + slidePrevTextContainer.className = "slideText"; + slidePrevTextContainer.innerText = slide.text; + slidePrevTextContainer.dir = "rtl"; + + divContainer.appendChild(slideThumbPreview); + divContainer.appendChild(slidePrevTextContainer); + + slideContainer.appendChild(divContainer); + + return slideContainer; + } + + /** + * Creates sidebar preview Object for a slide + * @param {Slide} slide + * @return {HTMLLIElement[]} + */ + #createLyricsPreview(slide) { + let lyricsLines = slide.splitText(this._mode, this._sepBy); + return lyricsLines.map((line, i) => { + //
  • ها بطيب
  • + let liEl = document.createElement("li"); + liEl.textContent = line; + liEl.dataset.textslidenumber = i; + liEl.dir = "rtl"; + return liEl; + }); + } + + /** + * Create a Presenter View. + * @param {presenter_Props} props - presentation props + */ + constructor(props) { + super(props); + this.#sideBarSlidesContainer = props.sidebarSlidesContainer; + this.#lyricsContainer = props.lyricsContainer; + + let addSlideBtn = document.querySelector( + `#${this.#sideBarSlidesContainer.id}>:first-child` + ); + + this._slides.forEach((slide) => { + this.#sideBarSlidesContainer.insertBefore( + this.#createSlideSidebarPreview(slide), + addSlideBtn + ); + }); + + this.#lyricsContainer.replaceChildren( + ...this.#createLyricsPreview(this._slides[0]) + ); + + this.#sideBarSlidesContainer.children[0].classList.add("active"); + this.#lyricsContainer.children[0].classList.add("active-text-slide"); + } + + /** + * Changes the slides dynamically the lyrics first then the video + * @param number + * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} + */ + + changeSlide(number = 1) { + let scrollBehaviour = { behavior: "smooth", block: "center" }; + let { currentSlide, currentTextSlide, isNewSlide } = super.changeSlide( + number + ); + + if (isNewSlide) { + let currentActive = this.#sideBarSlidesContainer.children[currentSlide]; + this.#lyricsContainer.replaceChildren( + ...this.#createLyricsPreview(this._slides[currentSlide]) + ); + this.#lyricsContainer.children[currentTextSlide].classList.add( + "active-text-slide" + ); + currentActive.classList.add("active"); + currentActive.scrollIntoView(scrollBehaviour); + this.#sideBarSlidesContainer.children[ + currentSlide - number + ].classList.remove("active"); + } else { + this.#lyricsContainer.children[currentTextSlide].classList.add( + "active-text-slide" + ); + this.#lyricsContainer.children[currentTextSlide].scrollIntoView( + scrollBehaviour + ); + if (this.#lyricsContainer.children.length > 1) { + this.#lyricsContainer.children[ + currentTextSlide - number + ].classList.remove("active-text-slide"); + } } + return { currentSlide, currentTextSlide, isNewSlide }; + } } -export default ShowPresenterView; \ No newline at end of file +export default ShowPresenterView; diff --git a/src/renderer/js/Classes/SidebarRendererClass.js b/src/renderer/js/Classes/SidebarRendererClass.js new file mode 100644 index 0000000..23aa894 --- /dev/null +++ b/src/renderer/js/Classes/SidebarRendererClass.js @@ -0,0 +1,119 @@ +class SidebarRenderer { + /** + * Creates an instance of SidebarRenderer. + * @param {HTMLElement} container - The DOM element that will contain the sidebar. + */ + constructor(props = { container: undefined, onSlideClickfn: undefined }) { + this.container = props.container; + this.onSlideClick = props.onSlideClickfn; + console.log(`${this.constructor.name} initialized `); + } + + _attachEventListeners() { + this.container.addEventListener("change", (e) => { + this.onSlideClick?.(e.target.dataset.slideId); + }); + } + + #createSlideElement(slide, index) { + const label = document.createElement("label"); + label.className = "slide-item"; + label.slideId = index; + + const radio = document.createElement("input"); + radio.type = "radio"; + radio.name = "slides"; + radio.id = "s" + index; + radio.className = "visually-hidden"; + radio.dataset.slideId = index; + + const preview = document.createElement("div"); + preview.className = "slide-preview ratio-16x9 w-100"; + preview.style.height = "10rem"; + preview.dataset.slideId = index; + + const icon = document.createElement("span"); + icon.className = "material-symbols-outlined"; + icon.innerText = "hide_image"; + preview.appendChild(icon); + + const num = document.createElement("span"); + num.className = "slide-number"; + num.innerText = index; + preview.appendChild(num); + + const textPreview = document.createElement("div"); + textPreview.className = "slide-text-preview"; + textPreview.innerHTML = slide.text || "Empty slide"; + textPreview.dataset.slideId = index; + + const content = document.createElement("div"); + content.className = "slide-content"; + content.appendChild(textPreview); + + label.appendChild(radio); + label.appendChild(preview); + label.appendChild(content); + + return label; + } + + #insertSlideElement(slideEl, index) { + const beforeEl = this.container.children[index]; + this.container.insertBefore(slideEl, beforeEl || null); + slideEl.querySelector("input").checked = true; + this.rerenderAllSlideNumbers(); + slideEl.scrollIntoView({ behavior: "smooth", block: "start" }); + } + + addSlideElement(slide, index) { + const slideEl = this.#createSlideElement(slide, index); + this.#insertSlideElement(slideEl, index); + if (slide.videoFileName) { + this.rerenderSlideThumbnail(index, slide); + } + } + + removeSlideElement(index, activeSlideIndex) { + console.log("Sidebar before removal", [...this.container.children]); + this.container.removeChild(this.container.children[index]); + this.rerenderAllSlideNumbers(); // ✅ update after removal + this.container.children[activeSlideIndex].querySelector( + "input" + ).checked = true; + } + + rerenderSlideElementText(index, text) { + console.log(this.container.children[index]); + this.container.children[index].querySelector( + ".slide-text-preview" + ).innerText = text; + } + + rerenderSlideThumbnail(index, slide) { + this.container.children[index].querySelector( + ".slide-preview" + ).style.backgroundImage = `url('media://${slide.videoFileName}.${slide.videoThumbnailFormat}')`; + this.container.children[index].querySelector( + ".material-symbols-outlined" + ).innerText = ``; + } + + rerenderAllSlideNumbers() { + Array.from(this.container.children).forEach((slideEl, i) => { + const slidePreview = slideEl.querySelector(".slide-preview"); + if (slidePreview) slidePreview.dataset.slideId = i; + const slideTextPreview = slideEl.querySelector(".slide-text-preview"); + if (slideTextPreview) slideTextPreview.dataset.slideId = i; + const radioBtn = slideEl.querySelector("input"); + if (radioBtn) radioBtn.dataset.slideId = i; + const numberSpan = slideEl.querySelector(".slide-number"); + if (numberSpan) numberSpan.innerText = i + 1; + }); + } + + clear() { + this.container.innerHTML = ""; + } +} +export default SidebarRenderer; diff --git a/src/renderer/js/Classes/Slide.ts b/src/renderer/js/Classes/Slide.ts new file mode 100644 index 0000000..83144be --- /dev/null +++ b/src/renderer/js/Classes/Slide.ts @@ -0,0 +1,188 @@ +interface FontBackground { + color: string; + opacity: number; +} + +interface Font { + family?: string; + bold?: boolean; + textToHeightRatio?: number; + background: FontBackground | false; +} + +interface SlideJSON { + video: { + name?: string; + format?: string; + muted?: boolean; + }; + text?: { + x: number; + y: number; + font: Font; + value: string; + }; + thumbnail?: { + format: string; + }; +} + +class Slide { + private _text: string; + private _textX: number; + private _textY: number; + private _loop: boolean; + private _muted: boolean; + + private _fontFamily: string; + private _fontBold: boolean; + private _fontTextToHeightRatio: number; + private _fontBackground: FontBackground | false; + + private _videoThumbnailFormat: string; + private _videoFileName: string; + private _videoFileFormat: string; + + constructor( + props: SlideJSON = { + video: { name: undefined, muted: true, format: "mp4" }, + } + ) { + this._videoFileName = props?.video?.name?.replace(/\.[^/.]+$/, ""); + this._videoFileFormat = props?.video?.format || "mp4"; + + this._videoThumbnailFormat = props?.thumbnail?.format || "png"; + + this._text = props?.text?.value; + this._textX = props?.text?.x || 0; + this._textY = props?.text?.y || 0; + + this._muted = props.video.muted; + + this._fontFamily = props?.text?.font?.family || "Calibri"; + this._fontBold = props?.text?.font?.bold || true; + this._fontTextToHeightRatio = props?.text?.font?.textToHeightRatio || 0.125; + + this._fontBackground = props?.text?.font?.background || { + color: "black", + opacity: 0.5, + }; + } + + get text() { + return this._text; + } + get fontBackground() { + return this._fontBackground; + } + get fontFamily() { + return this._fontFamily; + } + get textPosition() { + return { x: this._textX, y: this._textY }; + } + /** + * Sets the position of the text overlay on the slide. + * + * @param prop - An object containing the `x` and `y` coordinates for the text position. + * @property prop.x - The horizontal position of the text. + * @property prop.y - The vertical position of the text. + */ + setTextPosition(prop: { x: number; y: number }) { + this._textX = prop.x; + this._textY = prop.y; + } + + get videoThumbnailFormat(): string { + return this._videoThumbnailFormat; + } + get videoFileFormat(): string { + return this._videoFileFormat; + } + get videoFileName(): string { + return this._videoFileName; + } + get loop(): boolean { + return this._loop; + } + + get isMuted(): boolean { + return this._muted; + } + + toggleMuted(): Boolean { + this._muted = !this._muted; + return this._muted; + } + toggleBackground(): FontBackground | false { + if (this._fontBackground) { + this._fontBackground = false; + return this._fontBackground; + } + this._fontBackground = { + color: "black", + opacity: 0.5, + }; + return this._fontBackground; + } + + setText(text: string): void { + this._text = text; + } + setFontName(name: string): void { + this._fontFamily = name; + } + setVideoFileName(filename: string) { + this._videoFileName = filename.replace(/\.[^/.]+$/, ""); + } + + toJSON(): SlideJSON { + return { + video: { + name: this._videoFileName, + format: this._videoFileFormat, + muted: this._muted, + }, + text: { + x: this._textX, + y: this._textY, + font: { + family: this._fontFamily, + bold: this._fontBold, + textToHeightRatio: this._fontTextToHeightRatio, + background: this._fontBackground || false, + }, + value: this._text, + }, + thumbnail: { + format: this.videoThumbnailFormat, + }, + }; + } + + // this function is only added to prevent any breaks in presentation mode + /** + * this function is only added to prevent any breaks in presentation mode + * and it will be deprecated and removed in future version in favour of LyricManager + * @param mode + * @param sepBy + * @returns + */ + splitText(mode = "words", sepBy: any = "6") { + let subtitledText = []; + + if (mode === "words") { + sepBy *= 1; + let textSplit = this._text.split(" "); + for (let i = 0; i < textSplit.length; i += sepBy) { + subtitledText.push(textSplit.slice(i, i + sepBy).join(" ")); + } + } else { + subtitledText = this._text.split(sepBy); + } + console.log(subtitledText); + return subtitledText; + } +} + +export default Slide; diff --git a/src/renderer/js/Classes/SlideClass.js b/src/renderer/js/Classes/SlideClass.js deleted file mode 100644 index 86fc054..0000000 --- a/src/renderer/js/Classes/SlideClass.js +++ /dev/null @@ -1,182 +0,0 @@ -import slide from '../../asset/resource/Presentation1.png'; - -/** Class representing a slide. - * @class - */ -class Slide { - #videoFileFormat; - #videoFileName; - - get textX() { - return this._textX; - } - - get textY() { - return this._textY; - } - - get text() { - return this._text; - } - - get videoFileName() { - return this.#videoFileName; - } - - get videoFileFormat() { - return this.#videoFileFormat; - } - - get loop() { - return this._loop; - } - - set text(text) { - this._text = text; - } - - /** - * - * @param {string} videoFileName - */ - set videoFileName(videoFileName) { - let nameSplitted = videoFileName.split(".") - this.#videoFileFormat = nameSplitted.pop(); - console.log(nameSplitted, this.#videoFileFormat) - this.#videoFileName = nameSplitted.join("."); - } - - - /** - * - * @param {Object} params - * @param {string} params.text - * @param {string} params.videoFileName - * @param {number} params.X - * @param {number} params.Y - * @param {boolean} params.loop - * @param {string} params.videoFileFormat - * @param {string} params.videoThumbnailFormat - * @param {boolean} params.isRTL - */ - constructor({ - text = "Please Enter the text", - videoFileName = undefined, - isRTL = true, - X = 0, - Y = 0, - loop = true, - videoThumbnailFormat = "png", - videoFileFormat = "mp4" - } = {}) { - - /** @type{string} Video file name */ - this.#videoFileName = videoFileName; - /** @type{string} Video file format */ - this.#videoFileFormat = videoFileFormat; - /** @type{string} Video thumbnail format */ - this.videoThumbnailFormat = videoThumbnailFormat - /** @type{string} text attached to the video */ - this._text = text; - /** @type{number} text X position */ - this._textX = X || 0; - /** @type{number} text Y position */ - this._textY = Y || 0; - /** @type{boolean} weather to loop the video */ - this._loop = loop || true - }; - - /** - * Split text according to the delimiters - * @param {string} mode - * @param {string} sepBy - * @return {string[]} - */ - splitText(mode = "words", sepBy = "6") { - let subtitledText = [] - - if (mode === "words") { - sepBy *= 1 - let textSplit = this._text.split(" ") - for (let i = 0; i < textSplit.length; i += sepBy) { - subtitledText.push(textSplit.slice(i, i + sepBy).join(" ")) - } - - } else { - subtitledText = this._text.split(sepBy) - } - console.log(subtitledText) - return subtitledText - } - - - /** - * Create video thumbnail - * @param {string} basePath - * @param {number} thumbAtPercent - * @return {Promise} base64 Image - */ - - createVideoCoverImage(basePath, thumbAtPercent = 0.2) { - return new Promise((resolve, reject) => { - // define a canvas to have the same dimension as the video - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = 1920; - canvas.height = 1080; - if (this.videoFileName !== undefined) { - // load the file to a video player - const videoPlayer = document.createElement('video'); - videoPlayer.setAttribute('src', "file://" + basePath + "/" + this.videoFileName + "." + this.videoFileFormat); - videoPlayer.load(); - videoPlayer.addEventListener('error', (ex) => { - reject("error when loading video file", ex); - }); - // load metadata of the video to get video duration and dimensions - videoPlayer.addEventListener('loadedmetadata', () => { - // seek to user defined timestamp (in seconds) if possible - if (videoPlayer.duration < thumbAtPercent) { - reject("video is too short."); - return; - } - // delay seeking or else 'seeked' event won't fire on Safari - setTimeout(() => { - videoPlayer.currentTime = videoPlayer.duration * thumbAtPercent; - }, 200); - // extract video thumbnail once seeking is complete - videoPlayer.addEventListener('seeked', () => { - console.log('video is now paused at %ss.', thumbAtPercent); - // draw the video frame to canvas - ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height); - resolve(canvas.toDataURL('image/' + this.videoThumbnailFormat, 0.8)); - }); - }); - } else { - ctx.fillStyle = "#fff" - ctx.fillRect(0, 0, canvas.width, canvas.height); - resolve(canvas.toDataURL('image/' + this.videoThumbnailFormat, 0.8)) - } - }); - } - - - /** - * Converts Slide - * @return {Object} - */ - toJSON() { - return { - text: this._text, - videoFileName: this.#videoFileName, - videoFileFormat: this.#videoFileFormat, - videoThumbnailFormat: this.videoThumbnailFormat, - x: this.textX, - y: this.textY, - loop: this.loop, - } - } - - -} - -export default Slide; \ No newline at end of file diff --git a/src/renderer/js/Classes/SlideManager.js b/src/renderer/js/Classes/SlideManager.js new file mode 100644 index 0000000..bdedc93 --- /dev/null +++ b/src/renderer/js/Classes/SlideManager.js @@ -0,0 +1,106 @@ +import Slide from "./Slide"; + +/** + * Manages a collection of slides, providing methods to add, remove, and navigate slides. + * + * @class + * @classdesc SlideManager handles the storage and manipulation of Slide objects, maintaining the current slide index and providing accessors for slides and indices. + * + * @param {Slide[]} [initialSlides=[]] - Optional array of initial slides to populate the manager. + * + * @example + * const manager = new SlideManager([slide1, slide2]); + * manager.addSlide(slide3); + * manager.removeSlide(); + * console.log(manager.currentSlide); + */ +class SlideManager { + _slides = []; + #current = -1; + _onSlideChange; + + constructor(props) { + console.log(props); + this._slides = [...props.slides]; + this._onSlideChange = props.onSlideChange; + this.#current = this._slides.length > 0 ? 0 : -1; + console.log(`${this.constructor.name} initialized `, this._onSlideChange); + } + + addSlide(slide, index = Number(this.#current) + 1) { + // Handle edge case when no slides exist + if (this._slides.length === 0) { + this._slides.push(slide); + this.#current = 0; + console.log("Added first slide, new length:", this._slides.length); + this.setCurrent(this.#current); + return this.#current; + } + + index = Math.min(index, this._slides.length); + console.log("Adjusted index:", index); + + this._slides.splice(index, 0, slide); + this.setCurrent(index); + + return this.#current; + } + + removeSlide() { + if (this.#current === -1) return; + this._slides.splice(this.#current, 1); + let removedSlideIdx = this.#current; + this.setCurrent(Math.max(0, this.#current - 1)); + return removedSlideIdx; + } + + updateTextFont(fontName, index = this.#current) { + this._slides[index].setFontName(fontName); + } + updateTextPosition(x, y, index = this.#current) { + this._slides[index].setTextPosition({ x, y }); + } + updateSlideText(text, index = this.#current) { + this._slides[index].setText(text); + } + updateSlideVideo(videoName, index = this.#current) { + this._slides[index].setVideoFileName(videoName); + } + + /** + * Toggles the muted state of the slide at the specified index. + * If no index is provided, toggles the muted state of the current slide. + * + * @param {number} [index=this.#current] - The index of the slide to toggle mute on. + * @returns {boolean} - The new muted state of the slide (true if muted, false otherwise). + */ + toggleMuteSlide(index = this.#current) { + return this._slides[index].toggleMuted(); + } + + toggleSlideBackground(index = this.#current) { + return this._slides[index].toggleBackground(); + } + + get currentSlide() { + return this._slides[this.#current]; + } + + get currentIndex() { + return this.#current; + } + + setCurrent(index) { + this.#current = index; + this._onSlideChange?.(); + } + + get allSlides() { + return [...this._slides]; + } + + toJSON() { + return this._slides; + } +} +export default SlideManager; diff --git a/src/renderer/js/Classes/TextEditorClass.js b/src/renderer/js/Classes/TextEditorClass.js new file mode 100644 index 0000000..8a465f1 --- /dev/null +++ b/src/renderer/js/Classes/TextEditorClass.js @@ -0,0 +1,57 @@ +export default class TextEditorArea { + constructor(props) { + this.textArea = props.textAreaElement; + this.fontSelector = props.fontSelectorElement; + this.backgroundBtn = props.backgroundBtnElemnt; + // this.boldBtn = props.boldBtn; + // this.fontSizeField = props.fontSizeElement; + this.onTextEdited = props.onTextEditedFn; + this.onFontSelected = props.onFontSelectedFn; + this.onBackgroundToggle = props.onBackgroundToggle; + slideFiles.allFonts().then((fonts) => { + // console.log(fonts); + this.initializeFontSelector(fonts); + }); + } + + createFontOption(fontName, face) { + let option = document.createElement("option"); + const regex = new RegExp(`\\bbold\\b`, "gi"); + fontName = fontName.replace(regex, ""); + option.text = `${fontName} ابجد هوز`; + option.value = fontName; + option.style = `font-family: ${fontName}; padding:2px; font-size:18pt; `; + return option; + } + + initializeFontSelector(fonts) { + fonts.forEach((font) => { + this.fontSelector.appendChild(this.createFontOption(font)); + }); + } + _attachEventListeners() { + console.log(this.textArea); + this.textArea?.addEventListener("input", (e) => { + this.onTextEdited?.(e.currentTarget.value); + }); + this.fontSelector?.addEventListener("change", (e) => { + const fontName = e.target.value; + e.target.style = `font-family: ${fontName}; font-size: 16pt;`; + this.onFontSelected?.(fontName); + }); + this.backgroundBtn?.addEventListener("input", (e) => { + console.log(e.target.value); + this.onBackgroundToggle?.(e.target.value); + }); + } + + setTextArea(text) { + this.textArea.value = text; + } + renderBackgroundBtn(state) { + this.backgroundBtn.checked = state; + } + setFontSelector(fontName) { + this.fontSelector.value = fontName; + } +} diff --git a/src/renderer/js/Classes/Utils.js b/src/renderer/js/Classes/Utils.js new file mode 100644 index 0000000..2f7772a --- /dev/null +++ b/src/renderer/js/Classes/Utils.js @@ -0,0 +1,108 @@ +class Utils { + constructor(parameters) {} + + /** + * Create video thumbnail + * @param {string} basePath + * @param {number} thumbAtPercent + * @return {Promise} base64 Image + */ + + static createVideoCoverImage(filePath, thumbAtPercent = 0.2) { + return new Promise((resolve, reject) => { + // define a canvas to have the same dimension as the video + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = 1920; + canvas.height = 1080; + + // load the file to a video player + const videoPlayer = document.createElement("video"); + videoPlayer.setAttribute("src", "file://" + filePath); + videoPlayer.load(); + videoPlayer.addEventListener("error", (ex) => { + reject("error when loading video file", ex); + }); + // load metadata of the video to get video duration and dimensions + videoPlayer.addEventListener("loadedmetadata", () => { + // seek to user defined timestamp (in seconds) if possible + if (videoPlayer.duration < thumbAtPercent) { + reject("video is too short."); + return; + } + // delay seeking or else 'seeked' event won't fire on Safari + setTimeout(() => { + videoPlayer.currentTime = videoPlayer.duration * thumbAtPercent; + }, 200); + // extract video thumbnail once seeking is complete + videoPlayer.addEventListener("seeked", () => { + console.log("video is now paused at %ss.", thumbAtPercent); + // draw the video frame to canvas + ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height); + resolve(canvas.toDataURL("image/" + this.videoThumbnailFormat, 0.8)); + }); + }); + }); + } + + // Helper functions for calculating bounding boxes + static getCorner(pivotX, pivotY, diffX, diffY, angle) { + const distance = Math.sqrt(diffX * diffX + diffY * diffY); + + // Find angle from pivot to corner + angle += Math.atan2(diffY, diffX); + + // Get new x and y coordinates + const x = pivotX + distance * Math.cos(angle); + const y = pivotY + distance * Math.sin(angle); + + return { x, y }; + } + + // Calculate client rect accounting for rotation + static getClientRect(rotatedBox) { + const { x, y, width, height } = rotatedBox; + const rad = rotatedBox.rotation; + + const p1 = this.getCorner(x, y, 0, 0, rad); + const p2 = this.getCorner(x, y, width, 0, rad); + const p3 = this.getCorner(x, y, width, height, rad); + const p4 = this.getCorner(x, y, 0, height, rad); + + const minX = Math.min(p1.x, p2.x, p3.x, p4.x); + const minY = Math.min(p1.y, p2.y, p3.y, p4.y); + const maxX = Math.max(p1.x, p2.x, p3.x, p4.x); + const maxY = Math.max(p1.y, p2.y, p3.y, p4.y); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + + // Calculate total bounding box of multiple shapes + static getTotalBox(boxes) { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + boxes.forEach((box) => { + minX = Math.min(minX, box.x); + minY = Math.min(minY, box.y); + maxX = Math.max(maxX, box.x + box.width); + maxY = Math.max(maxY, box.y + box.height); + }); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } +} + +module.exports = Utils; diff --git a/src/renderer/js/Classes/VideoToolbarClass.js b/src/renderer/js/Classes/VideoToolbarClass.js new file mode 100644 index 0000000..5dbfc13 --- /dev/null +++ b/src/renderer/js/Classes/VideoToolbarClass.js @@ -0,0 +1,28 @@ +class VideoToolbar { + #muteButton; + #replaceVideoButton; + constructor(props) { + this.container = props.container; + this.onMuteBtnClicked = props.onMuteButton; + this.onReplaceBtnClicked = props.onReplaceBtn; + this.#muteButton = this.container.querySelector("#muteVideoBtn"); + this.#replaceVideoButton = this.container.querySelector("#replaceVideoBtn"); + + console.log(`${this.constructor.name} initialized `); + } + + _attachEventListeners() { + this.#muteButton.addEventListener( + "click", + this.onMuteBtnClicked.bind(this) + ); + } + + changeMuteButtonIcon(muted) { + console.log(this.#muteButton); + this.#muteButton.querySelector(".material-symbols-outlined").innerText = + muted ? "volume_off" : "volume_up"; + } +} + +export default VideoToolbar; diff --git a/src/renderer/openFileDialog/index.html b/src/renderer/openFileDialog/index.html index 4180c84..231ab91 100644 --- a/src/renderer/openFileDialog/index.html +++ b/src/renderer/openFileDialog/index.html @@ -37,8 +37,11 @@
    -
    - +
    + + + +
    diff --git a/src/renderer/openFileDialog/index.js b/src/renderer/openFileDialog/index.js index 39e9bbb..c3a1c74 100644 --- a/src/renderer/openFileDialog/index.js +++ b/src/renderer/openFileDialog/index.js @@ -1,21 +1,31 @@ -import "bootstrap" -import "./index.scss" -const selectFileBtn = document.querySelector("#selectFileBtn") -const openFileBtn = document.querySelector("#openFileBtn") -const fileDestField = document.querySelector("#fileDestContainer") +import "bootstrap"; +import "./index.scss"; +const selectFileBtn = document.querySelector("#selectFileBtn"); +const openFileBtn = document.querySelector("#openFileBtn"); +const presentFileBtn = document.querySelector("#presentFileBtn"); +const fileDestField = document.querySelector("#fileDestContainer"); //input[name=mode]:checked selectFileBtn.addEventListener("click", () => { - file.open("o").then(([dist])=>{ - fileDestField.value = dist - }) -}) + file.open("o").then(([dist]) => { + fileDestField.value = dist; + }); +}); -openFileBtn.addEventListener("click",()=>{ - if (fileDestField.value!== ""){ - let mode = document.querySelector("input[name=mode]:checked").value - let sepBy = document.querySelector(`#${mode}`).value - let filePath = fileDestField.value - let params = {sepBy, mode, filePath} - file.fileOpened(params) - } -}) \ No newline at end of file +openFileBtn.addEventListener("click", () => { + if (fileDestField.value !== "") { + let mode = document.querySelector("input[name=mode]:checked").value; + let sepBy = document.querySelector(`#${mode}`).value; + let filePath = fileDestField.value; + let params = { sepBy, mode, filePath, present: false }; + file.fileOpened(params); + } +}); +presentFileBtn.addEventListener("click", () => { + if (fileDestField.value !== "") { + let mode = document.querySelector("input[name=mode]:checked").value; + let sepBy = document.querySelector(`#${mode}`).value; + let filePath = fileDestField.value; + let params = { sepBy, mode, filePath, present: true }; + file.fileOpened(params); + } +}); diff --git a/src/renderer/preload.js b/src/renderer/preload.js index 7aeb485..8d2c263 100644 --- a/src/renderer/preload.js +++ b/src/renderer/preload.js @@ -2,29 +2,38 @@ // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts // Preload (Isolated World) -const {contextBridge, ipcRenderer} = require('electron') +const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("file", { - open: (mode) => ipcRenderer.invoke("file-dialog-open", mode), - save: (fileContent) => ipcRenderer.invoke("file-save", fileContent), - saveAndQuit: (fileContent) => ipcRenderer.invoke("save-quit", fileContent), - copyVideo: (vidPath) => ipcRenderer.invoke("copy-video", vidPath), - fileOpened: (fileParams) => ipcRenderer.invoke("file-opened", JSON.parse(JSON.stringify(fileParams))), - onFileParams: (callback) => ipcRenderer.on("file-params", (_event, fileParams) => callback(fileParams)) -}) + open: (mode) => ipcRenderer.invoke("file-dialog-open", mode), + fileOpened: (fileParams) => + ipcRenderer.invoke("file-opened", JSON.parse(JSON.stringify(fileParams))), + onFileParams: (callback) => + ipcRenderer.on("file-params", (_event, fileParams) => callback(fileParams)), + save: (fileContent) => ipcRenderer.invoke("file-save", fileContent), + onSaveBeforeQuit: (callback) => ipcRenderer.on("save-before-quit", callback), + saveAndQuit: (fileContent) => ipcRenderer.invoke("save-quit", fileContent), + saveDone: () => ipcRenderer.send("save-done"), +}); -contextBridge.exposeInMainWorld("thumbs", { - create: (props) => ipcRenderer.invoke("create-thumb", props) -}) +/* contextBridge.exposeInMainWorld("thumbs", { + create: (props) => ipcRenderer.invoke("create-thumb", props), +}); + */ -contextBridge.exposeInMainWorld("comm", { - toPresentation: (props) => ipcRenderer.send("to-presentation", props), - onSlideshowInitialized: (callback) => ipcRenderer.on("slideshow:init", (_e) => callback()), - startSlideshow: (content) => ipcRenderer.invoke("slideshow:start", content), - onSlideshowDestroy: (callback) => ipcRenderer.on("slideshow:destroy", (_e) => callback()), - -}) +contextBridge.exposeInMainWorld("slideFiles", { + addSlideFiles: (props) => ipcRenderer.send("addSlideFiles", props), + allFonts: () => ipcRenderer.invoke("getSystemFonts"), +}); +contextBridge.exposeInMainWorld("comm", { + toPresentation: (props) => ipcRenderer.send("to-presentation", props), + onSlideshowInitialized: (callback) => + ipcRenderer.on("slideshow:init", (_e) => callback()), + startSlideshow: (content) => ipcRenderer.invoke("slideshow:start", content), + onSlideshowDestroy: (callback) => + ipcRenderer.on("slideshow:destroy", (_e) => callback()), +}); /* ipcRenderer.on("file-opened",(event, basePath, fileContent)=>{ diff --git a/src/renderer/presentationView/renderer.js b/src/renderer/presentationView/renderer.js index f5104f0..aab8e8b 100644 --- a/src/renderer/presentationView/renderer.js +++ b/src/renderer/presentationView/renderer.js @@ -1,5 +1,5 @@ import Show from "../js/Classes/ShowClass"; -import Slide from "../js/Classes/SlideClass"; +import Slide from "../js/Classes/Slide"; import hotkeys from 'hotkeys-js'; import "./index.scss"; diff --git a/src/renderer/presenterView/js/fileOpen.js b/src/renderer/presenterView/js/fileOpen.js index 960caf2..90a9c14 100644 --- a/src/renderer/presenterView/js/fileOpen.js +++ b/src/renderer/presenterView/js/fileOpen.js @@ -1,4 +1,4 @@ -import Slide from "../../js/Classes/SlideClass"; +import Slide from "../../js/Classes/Slide"; import ShowPresenterView from "../../js/Classes/ShowPresenterViewClass"; import hotkeys from "hotkeys-js"; diff --git a/src/utils/MediaResponderClass.js b/src/utils/MediaResponderClass.js new file mode 100644 index 0000000..5ac38bc --- /dev/null +++ b/src/utils/MediaResponderClass.js @@ -0,0 +1,104 @@ +import fs from "fs"; +import path, { parse as pathParse } from "path"; +import mime from "mime"; + +/** + * Generic media responder with range support (videos, images, audio, etc.) + */ +class MediaResponder { + constructor(request, currentProject) { + this.request = request; + this.currentProject = currentProject; + this.headers = new Headers(); + this.rangeText = request.headers.get("range"); + this.status = 200; + } + + async handle() { + const filePath = decodeURIComponent( + this.request.url.slice("media://".length) + ); + const parsed = pathParse(filePath); + const ext = parsed.ext; + const baseName = parsed.name + ext; + + const mimeType = mime.getType(ext) || "application/octet-stream"; + this.headers.set("Content-Type", mimeType); + + const localFile = + this.currentProject.notInArchive[baseName] || + path.join(this.currentProject.projectTempFolder, "videos", baseName); + if (typeof localFile === "string") { + return this.#respondFromFile(localFile); + } + + if (localFile instanceof Uint8Array) { + return this.#respondFromBuffer(localFile); + } + + const zipEntryPath = `videos\\${baseName}`; + const buf = await this.currentProject.fileStream(zipEntryPath); + return this.#respondFromBuffer(buf); + } + + #parseRange(size) { + if (!this.rangeText) return null; + const match = this.rangeText.match(/bytes=(\d+)-(\d*)/); + if (!match) return null; + const start = parseInt(match[1], 10); + const end = match[2] ? parseInt(match[2], 10) : size - 1; + if (start >= size || end >= size) return null; + return { + start, + end, + length: end - start + 1, + rangeHeader: `bytes ${start}-${end}/${size}`, + }; + } + + #respondFromFile(filePath) { + const stat = fs.statSync(filePath); + const totalSize = stat.size; + const range = this.#parseRange(totalSize); + + if (range) { + this.headers.set("Accept-Ranges", "bytes"); + this.headers.set("Content-Length", `${range.length}`); + this.headers.set("Content-Range", range.rangeHeader); + this.status = 206; + const stream = fs.createReadStream(filePath, { + start: range.start, + end: range.end, + }); + return new Response(stream, { + headers: this.headers, + status: this.status, + }); + } + + this.headers.set("Content-Length", `${totalSize}`); + const stream = fs.createReadStream(filePath); + return new Response(stream, { headers: this.headers, status: this.status }); + } + + #respondFromBuffer(buffer) { + const totalSize = buffer.length; + const range = this.#parseRange(totalSize); + + if (range) { + this.headers.set("Accept-Ranges", "bytes"); + this.headers.set("Content-Length", `${range.length}`); + this.headers.set("Content-Range", range.rangeHeader); + this.status = 206; + return new Response(buffer.subarray(range.start, range.end), { + headers: this.headers, + status: this.status, + }); + } + + this.headers.set("Content-Length", `${totalSize}`); + return new Response(buffer, { headers: this.headers, status: this.status }); + } +} + +export default MediaResponder; diff --git a/src/workingFile.ts b/src/workingFile.ts new file mode 100644 index 0000000..6c5fd8e --- /dev/null +++ b/src/workingFile.ts @@ -0,0 +1,317 @@ +import { tmpdir } from "os"; +import * as path from "path"; +import * as fs from "fs"; +import * as archiver from "archiver"; +import * as tar from "tar-stream"; +import { buffer } from "stream/consumers"; +import { Stream } from "stream"; + +enum ProjectOpenMode { + NEW, + EDIT, + PRESENT, +} +interface addVideoSlideFileInterface { + imgBuffer: Buffer; + imgFileName: string; + videoFilePath: string; + videoFileName: string; +} +class WorkingFile { + /** + * shows the files that are added to the archive bit not available in the stream reader + * @type {Record} + */ + #notInArchive: Record = {}; + + #addedToArchive: string[] = []; + + /** + * @type {string} + */ + #sepMode; + + /** + * separation delimiter + * @type{number || string} + */ + #delimiter; + + /** + * Saved data content of the file JSON slideshow + */ + #lastSavedData: string; + + /** + * shows weather the file is opened or not + * @type{boolean} + */ + #isEditingOpened = false; + + /** + * Zip Object file + * + */ + #fileCreator: archiver.Archiver; + #writeStream: fs.WriteStream; + + #projectMode; + + /** + * the show file path + * @type {string} + */ + #filePath; + needsCreator: boolean; + get notInArchive() { + return this.#notInArchive; + } + + get isOpened() { + return this.#isEditingOpened; + } + + get projectPath() { + return this.#filePath; + } + + get basePath() { + const tmpAppPath = path.join(tmpdir(), "choirSlides"); + return tmpAppPath; + } + + /** + * File path parsed + * @type {ParsedPath} + */ + get #projectFilePathParsed() { + return path.parse(this.#filePath); + } + + get projectTempFolder() { + const projectTempPath = path.join(this.basePath, this.projectName); + if (!fs.existsSync(projectTempPath)) { + fs.mkdirSync(projectTempPath, { recursive: true }); + } + return projectTempPath; + } + + /** + * Opened file name + * @type {string} + */ + get projectName() { + return this.#projectFilePathParsed.name; + } + + /** + * Adds image and video files to the archive and tracks the video file path. + * + */ + + addVideoSlideFiles(props: addVideoSlideFileInterface) { + const { imgBuffer, imgFileName, videoFilePath, videoFileName } = props; + this.#fileCreator.append(imgBuffer, { name: `videos/${imgFileName}.png` }); + this.#fileCreator.file(videoFilePath, { name: `videos/${videoFileName}` }); + console.log("added Video Files"); + + this.#notInArchive[videoFileName.toLowerCase()] = videoFilePath; + this.#notInArchive[`${imgFileName}.png`.toLowerCase()] = imgBuffer; + } + + #extractProjectFile( + tarFilePath: string = this.#filePath, + outputDir: string = this.projectTempFolder + ) { + return new Promise((resolve, reject) => { + const extract = tar.extract(); + + extract.on("entry", (header, stream, next) => { + const outputPath = path.join(outputDir, header.name); + + // Ensure directories exist + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + const writeStream = fs.createWriteStream(outputPath); + stream.pipe(writeStream); + + writeStream.on("finish", next); // Wait until write completes + writeStream.on("error", reject); + stream.on("error", reject); + }); + + extract.on("finish", resolve); + extract.on("error", reject); + + fs.createReadStream(tarFilePath).pipe(extract); + }); + } + + constructor(data: any) { + this.#filePath = data.filePath || ""; + this.#sepMode = data.mode; + this.#delimiter = data.sepBy; + + if (data.present) { + this.#projectMode = ProjectOpenMode.PRESENT; + } else if (fs.existsSync(this.#filePath)) { + this.#projectMode = ProjectOpenMode.EDIT; + } else { + this.#projectMode = ProjectOpenMode.NEW; + } + + // Initialize based on mode + this.needsCreator = this.#projectMode !== ProjectOpenMode.PRESENT; + } + + async editProject() { + if (this.#projectMode === ProjectOpenMode.NEW) { + this.#lastSavedData = "[]"; + } else { + await this.#extractProjectFile(); + this.#lastSavedData = fs.readFileSync( + path.join(this.projectTempFolder, "slides.json"), + "utf-8" + ); + console.log( + path.join(this.projectTempFolder, "slides.json"), + this.#lastSavedData + ); + } + this.#isEditingOpened = true; + this.#fileCreator = archiver("tar", { + gzip: false, // Set to true if you want .tar.gz + }); + this.#writeStream = fs.createWriteStream(this.#filePath); + this.#fileCreator.pipe(this.#writeStream); + + this.#writeStream?.on("drain", () => { + console.log("Drained adding a file to zip"); + }); + this.#writeStream?.on("warning", (e: any) => { + console.log(`Warning while adding a file to zip: ${e.message}`); + }); + this.#writeStream?.on("finish", () => { + console.log("Finish adding a file to zip"); + }); + this.#writeStream?.on("close", () => { + console.log("closing zip"); + }); + this.#writeStream?.on("data", (data: any) => { + console.log("on data"); + }); + this.#writeStream?.on("entry", (data: any) => { + console.log("on entry"); + }); + } + + async saveProject(content: string) { + console.log("Saving"); + this.#addExtractedFilesToZip(); + console.log("writing"); + fs.writeFileSync(path.join(this.projectTempFolder, "slides.json"), content); + console.log("slides"); + this.#fileCreator.append(Buffer.from(content, "utf8"), { + name: "slides.json", + date: new Date(2025, 7, 8, 23, 4), + }); + + console.log(this.#fileCreator); + } + + async presentProject() { + const slidesStream: any = await this.fileStream("slides.json"); + this.#lastSavedData = (await buffer(slidesStream)).toString("utf8"); + } + + closeProject(slidesContent: string) { + console.log("Close Called"); + return new Promise((res, rej) => { + console.log("Created Promise"); + if (this.#isEditingOpened) { + console.log("Saving"); + this.#addExtractedFilesToZip(); + const slidesPath = path.join(this.projectTempFolder, "slides.json"); + fs.writeFileSync(slidesPath, slidesContent); + + this.#writeStream.on("finish", () => { + console.log(this.#fileCreator.pointer() + " total bytes"); + console.log( + "archiver has been finalized and the output file descriptor has closed." + ); + res(true); + }); + this.#writeStream.on("error", () => { + console.error("Error writing ZIP file:"); + rej("Error in Piping"); + }); + this.#writeStream.on("end", () => { + console.log("Data has been drained"); + }); + + this.#fileCreator.pipe(this.#writeStream); + console.log("piping"); + + console.log("finalizing"); + this.#fileCreator.finalize(); + } + }); + } + #addExtractedFilesToZip() { + const videosPath = path.join(this.projectTempFolder, "videos"); + const filesInPath = new Set( + fs.existsSync(videosPath) ? fs.readdirSync(videosPath) : [] + ); + const filesInArray = new Set(this.#addedToArchive); + const filesToBeAdded = [...filesInPath].filter( + (element: string) => !filesInArray.has(element) + ); + if (filesToBeAdded.length > 0) { + for (const f of filesToBeAdded) { + const fp = path.join(videosPath, f); + console.log({ fp, f }); + this.#fileCreator.append(fs.createReadStream(fp), { + name: `videos/${f}`, + }); + this.#addedToArchive.push(`${f}`); + } + } + } + + toObject() { + return { + filePath: this.#filePath, + sepBy: this.#delimiter, + mode: this.#sepMode, + content: this.#lastSavedData, + }; + } + + async fileStream(zipfilePath: string) { + return new Promise((resolve, reject) => { + const extract = tar.extract(); + const tarStream = fs.createReadStream(this.#filePath); + + let found = false; + + extract.on("entry", (header, stream, next) => { + if (header.name === zipfilePath) { + found = true; + resolve(stream); // Pass the file stream out + // Don't call `next()` here — let consumer drain the stream + } else { + stream.resume(); // Skip this entry + next(); + } + }); + + extract.on("finish", () => { + if (!found) reject(new Error(`File not found: ${zipfilePath}`)); + }); + + tarStream.pipe(extract); + }); + } +} + +export default WorkingFile; diff --git a/src/workingFile.js b/src/workingFileTemp.js similarity index 95% rename from src/workingFile.js rename to src/workingFileTemp.js index 024516b..faf5491 100644 --- a/src/workingFile.js +++ b/src/workingFileTemp.js @@ -69,7 +69,7 @@ class WorkingFile { } get #workingTempDir() { - return "temp-" + this.#projectRandom + return "videos" } /** @@ -124,13 +124,13 @@ class WorkingFile { } openProject() { - let zip = new AdmZip(this.#filePath) + /*let zip = new AdmZip(this.#filePath) zip.extractAllTo(this.#openedFileDirectory) fs.renameSync(path.join(this.#openedFileDirectory, "videos"), this.basePath) fs.renameSync(path.join(this.#openedFileDirectory, "slides.json"), this.#baseFilePath) fswin.setAttributesSync(this.basePath, {IS_HIDDEN: true}); - fswin.setAttributesSync(this.#baseFilePath, {IS_HIDDEN: true}); - this.#lastSavedData = fs.readFileSync(path.join(this.#openedFileDirectory, this.#projectRandom + ".json"), {encoding: "utf8"}) + fswin.setAttributesSync(this.#baseFilePath, {IS_HIDDEN: true});*/ + this.#lastSavedData = fs.readFileSync(this.#filePath, {encoding: "utf8"}) this.#isOpened = true console.log(`Opening ${this.#projectRandom}`) } @@ -161,6 +161,8 @@ class WorkingFile { } closeProject() { + this.#isOpened = false + return; if (this.#isOpened) { console.log(`Closing ${this.#projectRandom}`) fsp.rm(this.#baseFilePath).then(() => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..72cfccc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "es6", + "target": "es2024", + "jsx": "react", // If you are using React + "allowJs": true, + "moduleResolution": "node" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" // If you are using React +, "src/renderer/js/Classes/SlideManager.js" ] +} \ No newline at end of file diff --git a/webpack.main.config.js b/webpack.main.config.js index 276bcba..d685532 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -1,11 +1,15 @@ module.exports = { - /** - * This is the main entry point for your application, it's the first file - * that runs in the main process. - */ - entry: './src/index.js', - // Put your normal webpack config below here - module: { - rules: require('./webpack.rules'), - }, - }; \ No newline at end of file + /** + * This is the main entry point for your application, it's the first file + * that runs in the main process. + */ + entry: "./src/index.js", + + // Put your normal webpack config below here + module: { + rules: require("./webpack.rules"), + }, + resolve: { + extensions: [".ts", ".js"], + }, +}; diff --git a/webpack.renderer.config.js b/webpack.renderer.config.js index c363476..3a2d130 100644 --- a/webpack.renderer.config.js +++ b/webpack.renderer.config.js @@ -1,24 +1,22 @@ -const rules = require('./webpack.rules'); +const rules = require("./webpack.rules"); rules.push({ test: /\.scss$/, use: [ - { loader: 'style-loader' }, - { loader: 'css-loader' }, + { loader: "style-loader" }, + { loader: "css-loader" }, { - loader: 'sass-loader' + loader: "sass-loader", }, { - loader: 'postcss-loader', + loader: "postcss-loader", options: { postcssOptions: { plugins: function () { - return [ - require('autoprefixer') - ]; - } - } - } + return [require("autoprefixer")]; + }, + }, + }, }, ], }); @@ -29,7 +27,10 @@ module.exports = { rules, }, output: { - publicPath: './../', - assetModuleFilename:'[name][ext]' + publicPath: "./../", + assetModuleFilename: "[name][ext]", }, -}; \ No newline at end of file + resolve: { + extensions: [".ts", ".js"], + }, +}; diff --git a/webpack.rules.js b/webpack.rules.js index f6e0545..6f31aad 100644 --- a/webpack.rules.js +++ b/webpack.rules.js @@ -1,44 +1,38 @@ module.exports = [ - // Add support for native node modules - { - // We're specifying native_modules in the test because the asset relocator loader generates a - // "fake" .node file which is really a cjs file. - test: /native_modules[/\\].+\.node$/, - use: 'node-loader', + // Add support for native node modules + { + // We're specifying native_modules in the test because the asset relocator loader generates a + // "fake" .node file which is really a cjs file. + test: /native_modules[/\\].+\.node$/, + use: "node-loader", + }, + { + // Note: I dont have `svg` here because I run my .svg through the `@svgr/webpack` loader, + // but you can add it if you have no special requirements + test: /\.(gif|icns|ico|jpg|png|otf|eot|woff|woff2|ttf|svg)$/, + type: "asset/resource", + generator: { + outputPath: "./", }, - { - // Note: I dont have `svg` here because I run my .svg through the `@svgr/webpack` loader, - // but you can add it if you have no special requirements - test: /\.(gif|icns|ico|jpg|png|otf|eot|woff|woff2|ttf|svg)$/, - type: 'asset/resource', - generator:{ - outputPath:"./" - } - }, - { - test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, - parser: { amd: false }, - use: { - loader: '@vercel/webpack-asset-relocator-loader', - options: { - outputAssetBase: 'native_modules', - }, + }, + { + test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, + parser: { amd: false }, + use: { + loader: "@vercel/webpack-asset-relocator-loader", + options: { + outputAssetBase: "native_modules", }, }, - // Put your webpack loader rules in this array. This is where you would put - // your ts-loader configuration for instance: - /** - * Typescript Example: - * - * { - * test: /\.tsx?$/, - * exclude: /(node_modules|.webpack)/, - * loaders: [{ - * loader: 'ts-loader', - * options: { - * transpileOnly: true - * } - * }] - * } - */ - ]; \ No newline at end of file + }, + // Put your webpack loader rules in this array. This is where you would put + // your ts-loader configuration for instance: + /* * + * Typescript Example: + * */ + { + test: /\.ts$/, + include: /src/, + use: "ts-loader", + }, +]; From 3102d8c64392ff70ff1c4bfbc8e71c5f67d3ed92 Mon Sep 17 00:00:00 2001 From: Mark Samuel Date: Wed, 13 Aug 2025 09:25:27 +0300 Subject: [PATCH 2/2] [3.0.0] - 2025-08-13 ### Major Changes #### Added - **TypeScript Integration**: - Converted some core classes (Slide, WorkingFile) to TypeScript for better type safety - Added TypeScript configuration for future project-wide adoption - **Enhanced Text Customization**: - Font selection from system fonts (fixes #40) - Text positioning controls - Toggleable text background - **Video Controls**: - Mute/unmute functionality for videos - Improved video handling with black video fallback - **Project Management**: - New TAR-based project format replacing ZIP (fixes 2GB size limit) - Direct archive reading capability (fixes #32) #### Changed - **UI Improvements**: - Redesigned slide creator interface - Better slide thumbnail management - Improved sidebar navigation - **Architectural Updates**: - Refactored monolithic SlideCreator into modular components following SOLID principles - Implemented new MediaResponder class for better media handling - **Performance**: - Optimized video streaming and resource loading - Reduced memory usage for large projects #### Fixed - **Critical Issues**: - Resolved 2GB project size limitation - Fixed archive access problems - **Stability**: - Improved error handling during project save/load - Better resource cleanup when closing projects - **UX**: - Smoother transitions between slides - More reliable file operations ### Technical Details #### New Features - Added font selector with system font detection - Implemented text positioning with drag-and-drop - Created new video toolbar with mute control - Added project save confirmation before quit #### Code Improvements - TypeScript migration for core components - Modular architecture with separate classes for: - Canvas rendering - Sidebar management - Text editing - Video controls - Improved error handling and logging #### Dependency Updates - Updated Electron to latest stable version - Replaced JSZip with TAR for archive handling - Added new dev dependencies for TypeScript support --- CHANGELOG | 65 + extraResources/fontlist/LICENSE | 21 + extraResources/fontlist/README.md | 70 + extraResources/fontlist/demo.js | 15 + extraResources/fontlist/getSystemFonts.js | 21 + extraResources/fontlist/index.d.ts | 13 + extraResources/fontlist/index.js | 44 + extraResources/fontlist/libs/darwin/fontlist | Bin 0 -> 168560 bytes .../fontlist/libs/darwin/fontlist.m | 14 + extraResources/fontlist/libs/darwin/index.js | 68 + extraResources/fontlist/libs/linux/index.js | 28 + extraResources/fontlist/libs/standardize.js | 29 + .../fontlist/libs/win32/GetSystemFonts.ps1 | 61 + extraResources/fontlist/libs/win32/fonts.vbs | 29 + .../fontlist/libs/win32/getByPowerShell.js | 79 + .../fontlist/libs/win32/getByVBS.js | 79 + extraResources/fontlist/libs/win32/index.js | 34 + extraResources/fontlist/package.json | 20 + forge.config.js | 176 +- package-lock.json | 1426 ++++++++++++++++- package.json | 26 +- src/index.js | 474 +++--- src/renderer/ShowCreateView/index.html | 465 +++++- src/renderer/ShowCreateView/js/fileOpen.js | 100 +- ...24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + src/renderer/css/_style.scss | 29 +- .../js/Classes/CanvasRendererClass.js | 343 ++++ src/renderer/js/Classes/LyricManagerClass.js | 58 + src/renderer/js/Classes/ShowClass.js | 125 +- src/renderer/js/Classes/ShowCreator.js | 190 +++ src/renderer/js/Classes/ShowCreatorClass.js | 340 ---- .../js/Classes/ShowPresentationBaseClass.js | 411 ++--- .../js/Classes/ShowPresenterViewClass.js | 236 +-- .../js/Classes/SidebarRendererClass.js | 119 ++ src/renderer/js/Classes/Slide.ts | 188 +++ src/renderer/js/Classes/SlideClass.js | 182 --- src/renderer/js/Classes/SlideManager.js | 106 ++ src/renderer/js/Classes/TextEditorClass.js | 57 + src/renderer/js/Classes/Utils.js | 108 ++ src/renderer/js/Classes/VideoToolbarClass.js | 28 + src/renderer/openFileDialog/index.html | 7 +- src/renderer/openFileDialog/index.js | 46 +- src/renderer/preload.js | 45 +- src/renderer/presentationView/renderer.js | 2 +- src/renderer/presenterView/js/fileOpen.js | 2 +- src/utils/MediaResponderClass.js | 104 ++ src/workingFile.ts | 317 ++++ src/{workingFile.js => workingFileTemp.js} | 10 +- tsconfig.json | 15 + webpack.main.config.js | 24 +- webpack.renderer.config.js | 29 +- webpack.rules.js | 74 +- 52 files changed, 5029 insertions(+), 1524 deletions(-) create mode 100644 CHANGELOG create mode 100644 extraResources/fontlist/LICENSE create mode 100644 extraResources/fontlist/README.md create mode 100644 extraResources/fontlist/demo.js create mode 100644 extraResources/fontlist/getSystemFonts.js create mode 100644 extraResources/fontlist/index.d.ts create mode 100644 extraResources/fontlist/index.js create mode 100644 extraResources/fontlist/libs/darwin/fontlist create mode 100644 extraResources/fontlist/libs/darwin/fontlist.m create mode 100644 extraResources/fontlist/libs/darwin/index.js create mode 100644 extraResources/fontlist/libs/linux/index.js create mode 100644 extraResources/fontlist/libs/standardize.js create mode 100644 extraResources/fontlist/libs/win32/GetSystemFonts.ps1 create mode 100644 extraResources/fontlist/libs/win32/fonts.vbs create mode 100644 extraResources/fontlist/libs/win32/getByPowerShell.js create mode 100644 extraResources/fontlist/libs/win32/getByVBS.js create mode 100644 extraResources/fontlist/libs/win32/index.js create mode 100644 extraResources/fontlist/package.json create mode 100644 src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/renderer/js/Classes/CanvasRendererClass.js create mode 100644 src/renderer/js/Classes/LyricManagerClass.js create mode 100644 src/renderer/js/Classes/ShowCreator.js delete mode 100644 src/renderer/js/Classes/ShowCreatorClass.js create mode 100644 src/renderer/js/Classes/SidebarRendererClass.js create mode 100644 src/renderer/js/Classes/Slide.ts delete mode 100644 src/renderer/js/Classes/SlideClass.js create mode 100644 src/renderer/js/Classes/SlideManager.js create mode 100644 src/renderer/js/Classes/TextEditorClass.js create mode 100644 src/renderer/js/Classes/Utils.js create mode 100644 src/renderer/js/Classes/VideoToolbarClass.js create mode 100644 src/utils/MediaResponderClass.js create mode 100644 src/workingFile.ts rename src/{workingFile.js => workingFileTemp.js} (95%) create mode 100644 tsconfig.json diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..eb33db0 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,65 @@ +# Changelog + +## [3.0.0] - 2025-08-13 + +### Major Changes + +#### Added +- **TypeScript Integration**: + - Converted some core classes (Slide, WorkingFile) to TypeScript for better type safety + - Added TypeScript configuration for future project-wide adoption +- **Enhanced Text Customization**: + - Font selection from system fonts (fixes #40) + - Text positioning controls + - Toggleable text background +- **Video Controls**: + - Mute/unmute functionality for videos + - Improved video handling with black video fallback +- **Project Management**: + - New TAR-based project format replacing ZIP (fixes 2GB size limit) + - Direct archive reading capability (fixes #32) + +#### Changed +- **UI Improvements**: + - Redesigned slide creator interface + - Better slide thumbnail management + - Improved sidebar navigation +- **Architectural Updates**: + - Refactored monolithic SlideCreator into modular components following SOLID principles + - Implemented new MediaResponder class for better media handling +- **Performance**: + - Optimized video streaming and resource loading + - Reduced memory usage for large projects + +#### Fixed +- **Critical Issues**: + - Resolved 2GB project size limitation + - Fixed archive access problems +- **Stability**: + - Improved error handling during project save/load + - Better resource cleanup when closing projects +- **UX**: + - Smoother transitions between slides + - More reliable file operations + +### Technical Details + +#### New Features +- Added font selector with system font detection +- Implemented text positioning with drag-and-drop +- Created new video toolbar with mute control +- Added project save confirmation before quit + +#### Code Improvements +- TypeScript migration for core components +- Modular architecture with separate classes for: + - Canvas rendering + - Sidebar management + - Text editing + - Video controls +- Improved error handling and logging + +#### Dependency Updates +- Updated Electron to latest stable version +- Replaced JSZip with TAR for archive handling +- Added new dev dependencies for TypeScript support diff --git a/extraResources/fontlist/LICENSE b/extraResources/fontlist/LICENSE new file mode 100644 index 0000000..de813fd --- /dev/null +++ b/extraResources/fontlist/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 oldj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extraResources/fontlist/README.md b/extraResources/fontlist/README.md new file mode 100644 index 0000000..fda70c0 --- /dev/null +++ b/extraResources/fontlist/README.md @@ -0,0 +1,70 @@ +# font-list + +`font-list` is a Node.js package for listing the fonts available on your system. + +Current version supports **MacOS**, **Windows**, and **Linux**. + +## Install + +```bash +npm install font-list +``` + +## Usage + +```js +const fontList = require('font-list') + +fontList.getFonts() + .then(fonts => { + console.log(fonts) + }) + .catch(err => { + console.log(err) + }) +``` + +or like this in TypeScript: + +```ts +import { getFonts } from 'font-list' + +getFonts() + .then(fonts => { + console.log(fonts) + }) + .catch(err => { + console.log(err) + }) +``` + +The return value `fonts` is an Array, looks like: + +``` +[ '"Adobe Arabic"', + '"Adobe Caslon Pro"', + '"Adobe Devanagari"', + '"Adobe Fan Heiti Std"', + '"Adobe Fangsong Std"', + 'Arial', + ... + ] +``` + +If the font name contains spaces, the name will be wrapped in double quotes, otherwise there will be no double quotes, +for example: `'"Adobe Arabic"'`, `'Arial'`. + +If you don't want font names that contains spaces to be wrapped in double quotes, pass the options object +with `disableQuoting` set to true when calling the method `getFonts`: + +```js +const fontList = require('font-list') + +fontList.getFonts({ disableQuoting: true }) + .then(fonts => { + console.log(fonts) + }) + .catch(err => { + console.log(err) + }) +``` diff --git a/extraResources/fontlist/demo.js b/extraResources/fontlist/demo.js new file mode 100644 index 0000000..9435b1e --- /dev/null +++ b/extraResources/fontlist/demo.js @@ -0,0 +1,15 @@ +/** + * @author oldj + * @blog http://oldj.net + */ + +'use strict' + +require('./index').getFonts() + .then(fonts => { + //console.log(fonts) + console.log(fonts.join('\n')) + }) + .catch(err => { + console.log(err) + }) diff --git a/extraResources/fontlist/getSystemFonts.js b/extraResources/fontlist/getSystemFonts.js new file mode 100644 index 0000000..08f5625 --- /dev/null +++ b/extraResources/fontlist/getSystemFonts.js @@ -0,0 +1,21 @@ +const { getFonts } = require("./index"); + +function getSystemFonts() { + return new Promise((resolve, reject) => { + getFonts({ disableQuoting: true }) + .then((fonts) => { + fonts = [...new Set(fonts)]; + resolve(fonts || []); + }) + .catch((err) => { + resolve([]); + }); + }); +} + +(async () => { + const fonts = await getSystemFonts(); + // process 处理 + process.send(fonts); + // console.log('fonts', fonts); +})(); diff --git a/extraResources/fontlist/index.d.ts b/extraResources/fontlist/index.d.ts new file mode 100644 index 0000000..39ba5f8 --- /dev/null +++ b/extraResources/fontlist/index.d.ts @@ -0,0 +1,13 @@ +/** + * index.d.ts + * @author: oldj + * @homepage: https://oldj.net + */ + +interface IOptions { + disableQuoting: boolean; +} + +type FontList = string[] + +export function getFonts (options?: IOptions): Promise; diff --git a/extraResources/fontlist/index.js b/extraResources/fontlist/index.js new file mode 100644 index 0000000..3e3214f --- /dev/null +++ b/extraResources/fontlist/index.js @@ -0,0 +1,44 @@ +/** + * @author oldj + * @blog http://oldj.net + */ + +"use strict"; + +const standardize = require("./libs/standardize"); +const platform = process.platform; + +let getFontsFunc; +switch (platform) { + case "darwin": + getFontsFunc = require("./libs/darwin"); + break; + case "win32": + getFontsFunc = require("./libs/win32"); + break; + case "linux": + getFontsFunc = require("./libs/linux"); + break; + default: + throw new Error(`Error: font-list can not run on ${platform}.`); +} + +const defaultOptions = { + disableQuoting: false, +}; + +exports.getFonts = async (options) => { + options = Object.assign({}, defaultOptions, options); + + let fonts = await getFontsFunc(); + /* fonts = standardize(fonts, options) + + */ + fonts.sort((a, b) => { + return a.replace(/^['"]+/, "").toLocaleLowerCase() < + b.replace(/^['"]+/, "").toLocaleLowerCase() + ? -1 + : 1; + }); + return fonts; +}; diff --git a/extraResources/fontlist/libs/darwin/fontlist b/extraResources/fontlist/libs/darwin/fontlist new file mode 100644 index 0000000000000000000000000000000000000000..dcde35f505d5c393fab81f02d8d23a7546879f85 GIT binary patch literal 168560 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 +#import + +int main(int argc, const char * argv[]) { + @autoreleasepool { + NSFontManager *fontManager = [NSFontManager sharedFontManager]; + NSArray *fontFamilyNames = [[fontManager availableFontFamilies] sortedArrayUsingSelector:@selector(compare:)]; + + for (NSString *familyName in fontFamilyNames) { + printf("%s\n", [familyName UTF8String]); + } + } + return 0; +} diff --git a/extraResources/fontlist/libs/darwin/index.js b/extraResources/fontlist/libs/darwin/index.js new file mode 100644 index 0000000..0fba2ba --- /dev/null +++ b/extraResources/fontlist/libs/darwin/index.js @@ -0,0 +1,68 @@ +/** + * index + * @author oldj + * @blog https://oldj.net + */ + +'use strict' + +const path = require('path') +const execFile = require('child_process').execFile +const exec = require('child_process').exec +const util = require('util') + +const pexec = util.promisify(exec) + +const bin = path.join(__dirname, 'fontlist') +const font_exceptions = ['iconfont'] + +async function getBySystemProfiler () { + const cmd = `system_profiler SPFontsDataType | grep "Family:" | awk -F: '{print $2}' | sort | uniq` + const {stdout} = await pexec(cmd, {maxBuffer: 1024 * 1024 * 10}) + return stdout.split('\n').map(f => f.trim()).filter(f => !!f) +} + +async function getByExecFile () { + return new Promise(async (resolve, reject) => { + execFile(bin, {maxBuffer: 1024 * 1024 * 10}, (error, stdout, stderr) => { + if (error) { + reject(error) + return + } + + let fonts = [] + if (stdout) { + //fonts = fonts.concat(tryToGetFonts(stdout)) + fonts = fonts.concat(stdout.split('\n')) + } + if (stderr) { + //fonts = fonts.concat(tryToGetFonts(stderr)) + console.error(stderr) + } + + fonts = Array.from(new Set(fonts)) + .filter(i => i && !font_exceptions.includes(i)) + + resolve(fonts) + }) + }) +} + +module.exports = async () => { + let fonts = [] + try { + fonts = await getByExecFile() + } catch (e) { + console.error(e) + } + + if (fonts.length === 0) { + try { + fonts = await getBySystemProfiler() + } catch (e) { + console.error(e) + } + } + + return fonts +} diff --git a/extraResources/fontlist/libs/linux/index.js b/extraResources/fontlist/libs/linux/index.js new file mode 100644 index 0000000..f8bea32 --- /dev/null +++ b/extraResources/fontlist/libs/linux/index.js @@ -0,0 +1,28 @@ +/** + * index + * @author: oldj + * @homepage: https://oldj.net + */ + +const exec = require('child_process').exec +const util = require('util') + +const pexec = util.promisify(exec) + +async function binaryExists(binary) { + const { stdout } = await pexec(`whereis ${binary}`) + return stdout.length > (binary.length + 2) +} + +module.exports = async () => { + const fcListBinary = await binaryExists('fc-list') + ? 'fc-list' + : 'fc-list2' + + const cmd = fcListBinary + ' -f "%{family[0]}\\n"' + + const { stdout } = await pexec(cmd, { maxBuffer: 1024 * 1024 * 10 }) + const fonts = stdout.split('\n').filter(f => !!f) + + return Array.from(new Set(fonts)) +} diff --git a/extraResources/fontlist/libs/standardize.js b/extraResources/fontlist/libs/standardize.js new file mode 100644 index 0000000..e202b95 --- /dev/null +++ b/extraResources/fontlist/libs/standardize.js @@ -0,0 +1,29 @@ +/** + * @author oldj + * @blog http://oldj.net + */ + +'use strict' + +module.exports = function (fonts, options) { + fonts = fonts.map(i => { + // parse unicode names, eg: '"\\U559c\\U9e4a\\U805a\\U73cd\\U4f53"' -> '"喜鹊聚珍体"' + try { + i = i.replace(/\\u([\da-f]{4})/ig, (m, s) => String.fromCharCode(parseInt(s, 16))) + } catch (e) { + console.log(e) + } + + if (options && options.disableQuoting) { + if (i.startsWith('"') && i.endsWith('"')) { + i = `${i.substr(1, i.length - 2)}` + } + } else if (i.match(/[\s()+]/) && !i.startsWith('"')) { + i = `"${i}"` + } + + return i + }) + + return fonts +} diff --git a/extraResources/fontlist/libs/win32/GetSystemFonts.ps1 b/extraResources/fontlist/libs/win32/GetSystemFonts.ps1 new file mode 100644 index 0000000..afba40a --- /dev/null +++ b/extraResources/fontlist/libs/win32/GetSystemFonts.ps1 @@ -0,0 +1,61 @@ +# GetSystemFonts.ps1 +# Lists all system fonts with their localized names, available languages, and styles in JSON + +# Ensure UTF-8 output +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Add-Type -AssemblyName PresentationCore + +$fams = [Windows.Media.Fonts]::SystemFontFamilies +$result = @() + +$languageSamples = @{ + "en" = 0x0041 # Latin capital A + "ar" = 0x0627 # Arabic Alef +} +foreach ($fam in $fams) { + # Preferred family name (try zh-CN first, fallback to en-US) + $name = '' + if (-not $fam.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$name)) { + $name = $fam.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + + + # Supported language codes + $langs = @() + + foreach ($typeface in $fam.GetTypefaces()) { + try { + $glyph = New-Object Windows.Media.GlyphTypeface $typeface.FontUri + foreach ($lang in $languageSamples.Keys) { + if ($glyph.CharacterToGlyphMap.ContainsKey($languageSamples[$lang])) { + $langs += $lang + } + } + } + catch {} + } + + $langs = $langs | Sort-Object -Unique + + + # All typefaces (weights/styles) + $faces = @() + foreach ($tf in $fam.GetTypefaces()) { + $face = '' + if (-not $tf.FaceNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$face)) { + $face = $tf.FaceNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + if ([string]::IsNullOrWhiteSpace($face)) { + $face = "$($tf.Weight) $($tf.Style)" + } + $faces += $face + } + + $result += [PSCustomObject]@{ + name = $name + # faces = $faces + } +} + +$result | ConvertTo-Json -Depth 4 diff --git a/extraResources/fontlist/libs/win32/fonts.vbs b/extraResources/fontlist/libs/win32/fonts.vbs new file mode 100644 index 0000000..3126578 --- /dev/null +++ b/extraResources/fontlist/libs/win32/fonts.vbs @@ -0,0 +1,29 @@ +Option Explicit + +Dim objShell, objFSO, objFile, objFolder +Dim objFolderItem, colItems, objFont +Dim strFileName + + +Const FONTS = &H14& ' Fonts Folder + +' Instantiate Objects +Set objShell = CreateObject("Shell.Application") +Set objFolder = objShell.Namespace(FONTS) +Set objFolderItem = objFolder.Self +Set colItems = objFolder.Items +Set objFSO = CreateObject("Scripting.FileSystemObject") + +For Each objFont in colItems + WScript.StdOut.WriteLine(objFont.Path & vbtab & objFont.Name) +Next + +Set objShell = nothing +Set objFile = nothing +Set objFolder = nothing +Set objFolderItem = nothing +Set colItems = nothing +Set objFont = nothing +Set objFSO = nothing + +wscript.quit diff --git a/extraResources/fontlist/libs/win32/getByPowerShell.js b/extraResources/fontlist/libs/win32/getByPowerShell.js new file mode 100644 index 0000000..62c81c5 --- /dev/null +++ b/extraResources/fontlist/libs/win32/getByPowerShell.js @@ -0,0 +1,79 @@ +/** + * getByPowerShell + * @author: oldj + * @homepage: https://oldj.net + */ + +const exec = require("child_process").exec; + +const parse = (str) => { + return str + .split("\n") + .map((ln) => ln.trim()) + .filter((f) => !!f); +}; +/* +@see https://superuser.com/questions/760627/how-to-list-installed-font-families + + chcp 65001 | Out-Null + Add-Type -AssemblyName PresentationCore + $families = [Windows.Media.Fonts]::SystemFontFamilies + foreach ($family in $families) { + $name = '' + if (!$family.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$name)) { + $name = $family.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + echo $name + } +*/ +module.exports = () => + new Promise((resolve, reject) => { + let cmd = `chcp 65001|powershell -command "chcp 65001|Out-Null;Add-Type -AssemblyName PresentationCore;$families=[Windows.Media.Fonts]::SystemFontFamilies;foreach($family in $families){$name='';if(!$family.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'),[ref]$name)){$name=$family.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')]}echo $name}"`; + /* + cmd = `chcp 65001 | powershell -command "chcp 65001 | Out-Null; +Add-Type -AssemblyName PresentationCore; +$fams = [Windows.Media.Fonts]::SystemFontFamilies; +$result = @(); +foreach ($fam in $fams) { + # Pick preferred name + $name = ''; + if (-not $fam.FamilyNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$name)) { + $name = $fam.FamilyNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + + # Gather languages this font supports + $langs = $fam.FamilyNames.Keys | ForEach-Object { $_.IetfLanguageTag } + + # Gather all typefaces + $faces = @(); + foreach ($tf in $fam.GetTypefaces()) { + $face = ''; + if (-not $tf.FaceNames.TryGetValue([Windows.Markup.XmlLanguage]::GetLanguage('zh-cn'), [ref]$face)) { + $face = $tf.FaceNames[[Windows.Markup.XmlLanguage]::GetLanguage('en-us')] + } + if ([string]::IsNullOrWhiteSpace($face)) { + $face = "$($tf.Weight) $($tf.Style)" + } + $faces += $face + } + + $result += [PSCustomObject]@{ + name = $name + languages = $langs + faces = $faces + } +} +$result | ConvertTo-Json -Depth 4 -Encoding UTF8 +"`; */ + // const scriptPath = path.join(__dirname, "GetSystemFonts.ps1"); + + // execFile("powershell", ["-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8" },(err, stdout) => { + exec(cmd, { maxBuffer: 1024 * 1024 * 10 }, (err, stdout) => { + if (err) { + reject(err); + return; + } + + resolve(parse(stdout)); + }); + }); diff --git a/extraResources/fontlist/libs/win32/getByVBS.js b/extraResources/fontlist/libs/win32/getByVBS.js new file mode 100644 index 0000000..af12faa --- /dev/null +++ b/extraResources/fontlist/libs/win32/getByVBS.js @@ -0,0 +1,79 @@ +/** + * getByVBS + * @author: oldj + * @homepage: https://oldj.net + */ + +const os = require('os') +const fs = require('fs') +const path = require('path') +const execFile = require('child_process').execFile +const util = require('util') + +const p_copyFile = util.promisify(fs.copyFile) + +function tryToGetFonts(s) { + let a = s.split('\n') + if (a[0].includes('Microsoft')) { + a.splice(0, 3) + } + + a = a.map(i => { + i = i + .split('\t')[0] + .split(path.sep) + i = i[i.length - 1] + + if (!i.match(/^[\w\s]+$/)) { + i = '' + } + + i = i + .replace(/^\s+|\s+$/g, '') + .replace(/(Regular|常规)$/i, '') + .replace(/^\s+|\s+$/g, '') + + return i + }) + + return a.filter(i => i) +} + +async function writeToTmpDir(fn) { + let tmp_fn = path.join(os.tmpdir(), 'node-font-list-fonts.vbs') + await p_copyFile(fn, tmp_fn) + return tmp_fn +} + +module.exports = async () => { + let fn = path.join(__dirname, 'fonts.vbs') + + const is_in_asar = fn.includes('app.asar') + if (is_in_asar) { + fn = await writeToTmpDir(fn) + } + + return new Promise((resolve, reject) => { + let cmd = `cscript` + + execFile(cmd, [fn], { maxBuffer: 1024 * 1024 * 10 }, (err, stdout, stderr) => { + let fonts = [] + + if (err) { + reject(err) + return + } + + if (stdout) { + //require('electron').dialog.showMessageBox({message: 'stdout: ' + stdout}) + fonts = fonts.concat(tryToGetFonts(stdout)) + } + if (stderr) { + //require('electron').dialog.showMessageBox({message: 'stderr: ' + stderr}) + fonts = fonts.concat(tryToGetFonts(stderr)) + } + + resolve(fonts) + }) + }) +} diff --git a/extraResources/fontlist/libs/win32/index.js b/extraResources/fontlist/libs/win32/index.js new file mode 100644 index 0000000..f9d2dc2 --- /dev/null +++ b/extraResources/fontlist/libs/win32/index.js @@ -0,0 +1,34 @@ +/** + * index + * @author oldj + * @blog https://oldj.net + */ + +"use strict"; + +const os = require("os"); +const getByPowerShell = require("./getByPowerShell"); +const getByVBS = require("./getByVBS"); + +const methods_new = [getByPowerShell, getByVBS]; +const methods_old = [getByVBS, getByPowerShell]; + +module.exports = async () => { + let fonts = []; + + // @see {@link https://stackoverflow.com/questions/42524606/how-to-get-windows-version-using-node-js} + let os_v = parseInt(os.release()); + let methods = os_v >= 10 ? methods_new : methods_old; + console.log(os_v >= 10); + + for (let method of methods) { + try { + fonts = await method(); + if (fonts.length > 0) break; + } catch (e) { + console.log(e); + } + } + + return fonts; +}; diff --git a/extraResources/fontlist/package.json b/extraResources/fontlist/package.json new file mode 100644 index 0000000..3f84178 --- /dev/null +++ b/extraResources/fontlist/package.json @@ -0,0 +1,20 @@ +{ + "name": "font-list", + "version": "1.5.1", + "description": "list system fonts", + "main": "index.js", + "scripts": { + "demo": "node demo.js", + "test": "echo \"Error: no test specified\" && exit 1", + "p": "npm publish --access public" + }, + "keywords": [ + "font" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/oldj/node-font-list" + }, + "author": "oldj", + "license": "MIT" +} diff --git a/forge.config.js b/forge.config.js index 88b518a..23ae61d 100644 --- a/forge.config.js +++ b/forge.config.js @@ -1,87 +1,99 @@ + module.exports = { - packagerConfig: { - asar: { - unpackDir: "files" - }, - icon:'./src/icons/icon.ico', + packagerConfig: { + asar: { + unpackDir: "files", }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', config: {}, - }, - { - name: '@electron-forge/maker-zip', platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', config: {}, + icon: "./src/icons/icon.ico", + }, + rebuildConfig: {}, + makers: [ + { + name: "@electron-forge/maker-squirrel", + config: {}, + }, + { + name: "@electron-forge/maker-zip", + platforms: ["darwin"], + }, + { + name: "@electron-forge/maker-deb", + config: {}, + }, + { + name: "@electron-forge/maker-rpm", + config: {}, + }, + ], + plugins: [ + // { + // name: '@electron-forge/plugin-auto-unpack-natives', + // config: {}, + // }, + { + name: "@electron-forge/plugin-webpack", + config: { + mainConfig: "./webpack.main.config.js", + devContentSecurityPolicy: "media-src file:", + renderer: { + config: "./webpack.renderer.config.js", + entryPoints: [ + { + html: "./src/renderer/presenterView/index.html", + css: "./src/renderer/presenterView/index.scss", + js: "./src/renderer/presenterView/renderer.js", + name: "main_window", + preload: { + js: "./src/renderer/preload.js", + }, + }, + { + html: "./src/renderer/openFileDialog/index.html", + js: "./src/renderer/openFileDialog/index.js", + name: "open_file_dialog", + preload: { + js: "./src/renderer/preload.js", + }, + }, + { + html: "./src/renderer/saveFileDialog/index.html", + js: "./src/renderer/saveFileDialog/index.js", + name: "save_file_dialog", + preload: { + js: "./src/renderer/preload.js", + }, + }, + { + html: "./src/renderer/presentationView/index.html", + js: "./src/renderer/presentationView/renderer.js", + name: "presentation_view", + preload: { + js: "./src/renderer/preloadPresentationView.js", + }, + }, + { + html: "./src/renderer/ShowCreateView/index.html", + js: "./src/renderer/ShowCreateView/renderer.js", + name: "show_creator_view", + preload: { + js: "./src/renderer/preload.js", + }, + }, + ], }, - { - name: '@electron-forge/maker-rpm', config: {}, + }, + }, + ], + publishers: [ + { + name: "@electron-forge/publisher-github", + config: { + repository: { + owner: "marksamfd", + name: "VideoSlideshow", }, - ], plugins: [// { - // name: '@electron-forge/plugin-auto-unpack-natives', - // config: {}, - // }, - { - name: '@electron-forge/plugin-webpack', - config: { - mainConfig: './webpack.main.config.js', - devContentSecurityPolicy: "media-src file:", - renderer: { - config: './webpack.renderer.config.js', - entryPoints: [ - { - html: './src/renderer/presenterView/index.html', - css: './src/renderer/presenterView/index.scss', - js: './src/renderer/presenterView/renderer.js', - name: 'main_window', - preload: { - js: './src/renderer/preload.js' - } - }, - { - html: './src/renderer/openFileDialog/index.html', - js: './src/renderer/openFileDialog/index.js', - name: 'open_file_dialog', - preload: { - js: './src/renderer/preload.js' - } - }, - { - html: './src/renderer/saveFileDialog/index.html', - js: './src/renderer/saveFileDialog/index.js', - name: 'save_file_dialog', - preload: { - js: './src/renderer/preload.js' - } - }, { - html: './src/renderer/presentationView/index.html', - js: './src/renderer/presentationView/renderer.js', - name: 'presentation_view', - preload: { - js: './src/renderer/preloadPresentationView.js' - } - }, { - html: './src/renderer/ShowCreateView/index.html', - js: './src/renderer/ShowCreateView/renderer.js', - name: 'show_creator_view', - preload: { - js: './src/renderer/preload.js' - } - }] - }, - - } - }], - publishers: [{ - name: '@electron-forge/publisher-github', - config: { - repository: { - owner: 'marksamfd', - name: 'VideoSlideshow' - }, - prerelease: false - } - }] + prerelease: false, + }, + }, + ], }; diff --git a/package-lock.json b/package-lock.json index 9fc5be9..203ed44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,18 @@ { "name": "choirslides", - "version": "2.0.0", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "choirslides", - "version": "2.0.0", + "version": "2.0.6", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@popperjs/core": "^2.11.8", + "@types/archiver": "^6.0.3", + "@types/yazl": "^3.3.0", "adm-zip": "^0.5.14", "archiver": "^7.0.1", "bootstrap": "^5.3.3", @@ -19,12 +21,21 @@ "electron-log": "^5.1.5", "electron-reload": "^2.0.0-alpha.1", "electron-squirrel-startup": "^1.0.0", + "font-list": "^1.5.1", "fswin": "^3.24.524", "hotkeys-js": "^3.13.7", "interactjs": "^1.10.27", "konva": "^9.3.6", + "mime": "^4.0.4", "node-fetch": "^3.3.2", - "unzipper": "^0.12.1" + "node-stream-zip": "^1.15.0", + "progress-stream": "^2.0.0", + "tar": "^7.4.3", + "tar-stream": "^3.1.7", + "unzipper": "^0.12.1", + "video.js": "^8.17.4", + "videojs": "^1.0.0", + "yazl": "^3.3.1" }, "devDependencies": { "@electron-forge/cli": "^7.3.1", @@ -35,6 +46,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", "@electron-forge/plugin-webpack": "^7.3.1", "@electron-forge/publisher-github": "^6.3.0", + "@types/tar-stream": "^3.1.4", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "autoprefixer": "^10.4.19", "copy-webpack-plugin": "^12.0.2", @@ -46,7 +58,10 @@ "sass": "^1.74.1", "sass-loader": "^14.1.1", "style-loader": "^3.3.4", - "svg-inline-loader": "^0.8.2" + "svg-inline-loader": "^0.8.2", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" } }, "node_modules/@babel/code-frame": { @@ -157,6 +172,41 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@electron-forge/cli": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.3.1.tgz", @@ -831,6 +881,34 @@ "node": ">=12.13.0" } }, + "node_modules/@electron/rebuild/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/universal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", @@ -1041,6 +1119,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1414,6 +1513,43 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/archiver": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz", + "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==", + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1594,7 +1730,6 @@ "version": "20.12.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1625,6 +1760,15 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -1679,6 +1823,16 @@ "@types/node": "*" } }, + "node_modules/@types/tar-stream": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz", + "integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -1698,6 +1852,15 @@ "@types/node": "*" } }, + "node_modules/@types/yazl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-3.3.0.tgz", + "integrity": "sha512-mFL6lGkk2N5u5nIxpNV/K5LW3qVSbxhJrMxYGOOxZndWxMgCamr/iCsq/1t9kd8pEwhuNP91LC5qZm/qS9pOEw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vercel/webpack-asset-relocator-loader": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@vercel/webpack-asset-relocator-loader/-/webpack-asset-relocator-loader-1.7.3.tgz", @@ -1707,6 +1870,77 @@ "resolve": "^1.10.0" } }, + "node_modules/@videojs/http-streaming": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.3.tgz", + "integrity": "sha512-L7H+iTeqHeZ5PylzOx+pT3CVyzn4TALWYTJKkIc1pDaV/cTVfNGtG+9/vXPAydD+wR/xH1M9/t2JH8tn/DCT4w==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "4.0.0", + "aes-decrypter": "4.0.1", + "global": "^4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.3.0", + "mux.js": "7.0.3", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.14.0" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", + "integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -1857,7 +2091,6 @@ "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -1877,8 +2110,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -1925,6 +2157,19 @@ "acorn": "^8" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adm-zip": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.14.tgz", @@ -1933,6 +2178,30 @@ "node": ">=12.0" } }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2015,6 +2284,14 @@ "ajv": "^8.8.2" } }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "engines": { + "node": ">=0.4.2" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2086,6 +2363,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", @@ -2276,6 +2554,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2757,6 +3042,34 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -2822,6 +3135,14 @@ "tslib": "^2.0.3" } }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001606", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz", @@ -3054,6 +3375,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha512-QjQ1T4BqyHv19k6XSfdhy/QLlIOhywz0ekBUCa9h71zYMJlfDTGan/Z1JXzYkZ6v8R+GhvL/p4FZPbPW8WNXlg==", + "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", + "peer": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3085,6 +3420,15 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==", + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3400,6 +3744,13 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -3571,6 +3922,15 @@ "node": ">= 12" } }, + "node_modules/dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha512-AXvW8g7tO4ilk5HgOWeDmPi/ZPaCnMJ+9Cg1I3p19w6mcvAAXBuuGEXAxybC+Djj1PSZUiHUcyoYu7WneCX8gQ==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3588,6 +3948,14 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -3797,6 +4165,16 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -3842,6 +4220,11 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -4779,6 +5162,19 @@ "node": ">=8.0.0" } }, + "node_modules/esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -4826,6 +5222,12 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "peer": true + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -4937,6 +5339,15 @@ "which": "bin/which" } }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -5299,6 +5710,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha512-yjftfYnF4ThYEvKEV/kEFR15dmtyXTAh3vQnzpJUoc7Naj5y1P0Ck7Zs1+Vroa00E3KT3IYsk756S+8WA5dNLw==", + "peer": true, + "dependencies": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/findup-sync/node_modules/glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, + "dependencies": { + "inherits": "2", + "minimatch": "0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/findup-sync/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/findup-sync/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "node_modules/findup-sync/node_modules/minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "peer": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -5341,6 +5809,12 @@ } } }, + "node_modules/font-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/font-list/-/font-list-1.5.1.tgz", + "integrity": "sha512-Hr5V0dsSv91wH3FgirXd7qh1PydqA/vMQyWjFFWn+lUPJtC+3i2tzgVqbLRcvQh87TGdbTGbAR3mEo4VlwC1jw==", + "license": "MIT" + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -5614,6 +6088,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha512-hIGEBfnHcZpWkXPsAVeVmpYDvfy/matVl03yOY91FPmnpCC12Lm5izNxCjO3lHAeO6uaTwMxu7g450Siknlhig==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5652,6 +6135,15 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -5790,9 +6282,328 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "node_modules/grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha512-1iq3ylLjzXqz/KSq1OAE2qhnpcbkF2WyhsQcavZt+YmgvHu0EbPMEhGhy2gr0FP67isHpRdfwjB5WVeXXcJemQ==", + "peer": true, + "dependencies": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-contrib-uglify": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.2.7.tgz", + "integrity": "sha512-KXKM2UNLsCiUI6/DYfAIPm3i26UJJN6Cf6KD8fFa2TKllj7yLPC853IxtWBJ/3jX66QtXHGtdCORuuA6sAFvvA==", + "dependencies": { + "grunt-lib-contrib": "~0.6.1", + "uglify-js": "~2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "peerDependencies": { + "grunt": "~0.4.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha512-qYs/uM0ImdzwIXLhS4O5WLV5soAM+PEqqHI/hzSxlo450ERSccEhnXqoeDA9ZozOdaWuYnzTOTwRcVRogleMxg==", + "peer": true, + "dependencies": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha512-D0vbUX00TFYCKNZtcZzemMpwT8TR/FdRs1pmfiBw6qnUw80PfsjV+lhIozY/3eJ3PSG2zj89wd2mH/7f4tNAlw==", + "peer": true, + "dependencies": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-log-utils/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-log/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-log/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha512-cXPbfF8aM+pvveQeN1K872D5fRm30xfJWZiS63Y8W8oyIPLClCsmI8bW96Txqzac9cyL4lRqEBhbhJ3n5EzUUQ==", + "peer": true, + "dependencies": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-util/node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-util/node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha512-LVbt/rjK62gSbhehDVKL0vlaime4Y1IBixL+bKeNfoY4L2zab/jGrxU6Ka05tMA/zBxkTk5t3ivtphdyYupczw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-util/node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha512-E87fdQ/eRJr9W1X4wTPejNy9zTW3FI2vpCZSJ/HAY+TkjKVC0TUm1jk6vn2Z7qay0DQy0+RBGdXxj+RmmiGZKQ==", + "peer": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/grunt-lib-contrib": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-lib-contrib/-/grunt-lib-contrib-0.6.1.tgz", + "integrity": "sha512-HdCtJuMmmkSAVrAfsG7lZWE0YabrsPWwzcCCUgWQOAaQsQSUNhw/IwD2YjCSLh5y9NXSPzHTYFLL4ro7QbAJMA==", + "dependencies": { + "zlib-browserify": "0.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt/node_modules/argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha512-LjmC2dNpdn2L4UzyoaIr11ELYoLn37ZFy9zObrQFHsSuOepeUEMKnM8w5KL4Tnrp2gy88rRuQt6Ky8Bjml+Baw==", + "peer": true, + "dependencies": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + } + }, + "node_modules/grunt/node_modules/argparse/node_modules/underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha512-yxkabuCaIBnzfIvX3kBxQqCs0ar/bfJwDnFEHJUm/ZrRVhT3IItdRF5cZjARLzEnyQYtIUhsZ2LG2j3HidFOFQ==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, + "dependencies": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ==", + "deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/grunt/node_modules/iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/grunt/node_modules/inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA==", + "peer": true + }, + "node_modules/grunt/node_modules/js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha512-VEKcIksckDBUhg2JS874xVouiPkywVUh4yyUmLCDe1Zg3bCd6M+F1eGPenPeHLc2XC8pp9G8bsuofK0NeEqRkA==", + "peer": true, + "dependencies": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/grunt/node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha512-LVbt/rjK62gSbhehDVKL0vlaime4Y1IBixL+bKeNfoY4L2zab/jGrxU6Ka05tMA/zBxkTk5t3ivtphdyYupczw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "node_modules/grunt/node_modules/minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "peer": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "peer": true, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/grunt/node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha512-E87fdQ/eRJr9W1X4wTPejNy9zTW3FI2vpCZSJ/HAY+TkjKVC0TUm1jk6vn2Z7qay0DQy0+RBGdXxj+RmmiGZKQ==", + "peer": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, @@ -5880,6 +6691,15 @@ "node": ">=0.10.0" } }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -6381,6 +7201,11 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6664,9 +7489,9 @@ } }, "node_modules/konva": { - "version": "9.3.6", - "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.6.tgz", - "integrity": "sha512-dqR8EbcM0hjuilZCBP6xauQ5V3kH3m9kBcsDkqPypQuRgsXbcXUrxqYxhNbdvKZpYNW8Amq94jAD/C0NY3qfBQ==", + "version": "9.3.22", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.22.tgz", + "integrity": "sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==", "funding": [ { "type": "patreon", @@ -6680,7 +7505,8 @@ "type": "github", "url": "https://github.com/sponsors/lavrton" } - ] + ], + "license": "MIT" }, "node_modules/launch-editor": { "version": "2.6.1", @@ -6959,6 +7785,36 @@ "node": ">=12" } }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, + "node_modules/m3u8-parser/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -7090,15 +7946,17 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { @@ -7140,6 +7998,14 @@ "node": ">=4" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -7269,6 +8135,20 @@ "node": ">=10" } }, + "node_modules/mpd-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", + "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7288,6 +8168,22 @@ "multicast-dns": "cli.js" } }, + "node_modules/mux.js": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz", + "integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -7427,6 +8323,34 @@ "node": "^12.13 || ^14.13 || >=16" } }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7457,6 +8381,18 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -8014,6 +8950,17 @@ "node": ">=0.10.0" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8285,6 +9232,16 @@ "node": ">=0.4.0" } }, + "node_modules/progress-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz", + "integrity": "sha512-xJwOWR46jcXUq6EH9yYyqp+I52skPySOeHfkxOZ2IY1AiBi/sFJhbhAKHoV3OTw/omQ45KTio9215dRJ2Yxd3Q==", + "license": "BSD-2-Clause", + "dependencies": { + "speedometer": "~1.0.0", + "through2": "~2.0.3" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -8623,6 +9580,11 @@ "node": ">= 10.13.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -9056,6 +10018,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9280,6 +10254,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "peer": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9457,6 +10437,12 @@ "wbuf": "^1.7.3" } }, + "node_modules/speedometer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz", + "integrity": "sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==", + "license": "MIT" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -9724,39 +10710,85 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/temp": { @@ -9920,6 +10952,46 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -10000,6 +11072,81 @@ "node": ">=0.8.0" } }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -10031,11 +11178,88 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha512-tktIjwackfZLd893KGJmXc1hrRHH1vH9Po3xFh1XBjjeGAnN02xJ3SuoA+n1L29/ZaCA18KzCFlckS+vfPugiA==", + "dependencies": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/uglify-js/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/uglify-js/node_modules/source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha512-yfCwDj0vR9RTwt3pEzglgb3ZgmcXHt6DjG3bjJvzPwTL+5zDQ2MhmSzAcTy0GTiQuCiriSWXvWM1/NhKdXuoQA==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-js/node_modules/yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha512-5j382E4xQSs71p/xZQsU1PtRA2HXPAjX0E0DkoGLxwNASMOKX6A9doV1NrZmj85u2Pjquz402qonBzz/yLPbPA==", + "dependencies": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==" + }, + "node_modules/underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==", + "peer": true + }, + "node_modules/underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha512-3FVmhXqelrj6gfgp3Bn6tOavJvW0dNH2T+heTD38JRxIrAbiuzbqjknszoOYj3DyFB1nWiLj208Qt2no/L4cIA==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -10165,6 +11389,11 @@ "punycode": "^2.1.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "node_modules/username": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", @@ -10207,6 +11436,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -10226,6 +11462,62 @@ "node": ">= 0.8" } }, + "node_modules/video.js": { + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.4.tgz", + "integrity": "sha512-AECieAxKMKB/QgYK36ci50phfpWys6bFT6+pGMpSafeFYSoZaQ2Vpl83T9Qqcesv4TO7oNtiycnVeaBnrva2oA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "3.13.3", + "@videojs/vhs-utils": "^4.0.0", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.1", + "global": "4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.2.2", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/videojs/-/videojs-1.0.0.tgz", + "integrity": "sha512-FwI02jJ7d4E6goWuc/4LTN5OJlD1M0jInoIoNemo4EzMfu6IywhahMXDriLObX17ML62RsHS0oiCUE9wVB6i8A==", + "deprecated": "This is a placeholder package, please use the official 'video.js' package", + "dependencies": { + "grunt-contrib-uglify": "^0.2.7" + } + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==" + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "dependencies": { + "global": "^4.3.1" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -10564,6 +11856,14 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -10574,6 +11874,14 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10644,6 +11952,15 @@ "node": ">=8.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/xterm": { "version": "4.19.0", "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", @@ -10807,6 +12124,34 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yazl": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz", + "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "node_modules/yazl/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -10869,6 +12214,11 @@ "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } + }, + "node_modules/zlib-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.1.tgz", + "integrity": "sha512-fheIDCKXU0YAGZMv4FFwVTBMQRSv2ZjNqRN1VkZjetZDK/BC/hViEhasTh0kTeogcsIAl5gYE04GN53trT+cFw==" } } } diff --git a/package.json b/package.json index e92caca..74f537a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "choirslides", "productName": "ChoirSlides", - "version": "2.0.6", + "version": "3.0.0", "description": "A software designed to create lyric slideshows. This software allows users to combine lyrics with background videos, resulting in a visually engaging slideshow that synchronizes the lyrics with the video footage.", "main": "./.webpack/main", "scripts": { @@ -25,6 +25,8 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@popperjs/core": "^2.11.8", + "@types/archiver": "^6.0.3", + "@types/yazl": "^3.3.0", "adm-zip": "^0.5.14", "archiver": "^7.0.1", "bootstrap": "^5.3.3", @@ -33,12 +35,21 @@ "electron-log": "^5.1.5", "electron-reload": "^2.0.0-alpha.1", "electron-squirrel-startup": "^1.0.0", + "font-list": "^1.5.1", "fswin": "^3.24.524", "hotkeys-js": "^3.13.7", "interactjs": "^1.10.27", "konva": "^9.3.6", + "mime": "^4.0.4", "node-fetch": "^3.3.2", - "unzipper": "^0.12.1" + "node-stream-zip": "^1.15.0", + "progress-stream": "^2.0.0", + "tar": "^7.4.3", + "tar-stream": "^3.1.7", + "unzipper": "^0.12.1", + "video.js": "^8.17.4", + "videojs": "^1.0.0", + "yazl": "^3.3.1" }, "devDependencies": { "@electron-forge/cli": "^7.3.1", @@ -49,6 +60,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", "@electron-forge/plugin-webpack": "^7.3.1", "@electron-forge/publisher-github": "^6.3.0", + "@types/tar-stream": "^3.1.4", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "autoprefixer": "^10.4.19", "copy-webpack-plugin": "^12.0.2", @@ -60,6 +72,12 @@ "sass": "^1.74.1", "sass-loader": "^14.1.1", "style-loader": "^3.3.4", - "svg-inline-loader": "^0.8.2" - } + "svg-inline-loader": "^0.8.2", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + }, + "extraResources": [ + "./extraResources/**" + ] } diff --git a/src/index.js b/src/index.js index 8614d0c..8f52379 100644 --- a/src/index.js +++ b/src/index.js @@ -1,330 +1,244 @@ -const {app, BrowserWindow, ipcMain, dialog, protocol, net} = require('electron'); -const path = require('path'), fs = require("fs") -const {pathToFileURL} = require('url') +const { app, BrowserWindow, ipcMain, dialog, protocol } = require("electron"); +const path = require("path"); const electron = require("electron"); -import {createPresentationView, createShowCreatorView, createPresenterView} from './createViews' -import WorkingFile from './workingFile' -import log from 'electron-log/main'; +import { + createPresentationView, + createShowCreatorView, + createPresenterView, +} from "./createViews"; +import MediaResponder from "./utils/MediaResponderClass"; +import WorkingFile from "./workingFile"; -let VidFilestream; +import log from "electron-log/main"; +import cp from "child_process"; + +const EXTRARESOURCES_PATH = app.isPackaged + ? path.join(process.resourcesPath, "extraResources") + : path.join(__dirname, "../../extraResources"); + +const getExtraResourcesPath = (...paths) => { + return path.join(EXTRARESOURCES_PATH, ...paths); +}; if (app.isPackaged) { - log.initialize({spyRendererConsole: true}); - log.transports.file.format = '[{h}:{i}:{s}.{ms}] [{processType}] {text}'; -// log.transports.console.level = false; - Object.assign(console, log.functions); + log.initialize({ spyRendererConsole: true }); + log.transports.file.format = "[{h}:{i}:{s}.{ms}] [{processType}] {text}"; + // log.transports.console.level = false; + Object.assign(console, log.functions); } /** * The open project Now * @type {WorkingFile} */ -let currentProject = new WorkingFile({filePath: pathToFileURL(process.cwd())}); +let currentProject; let presentationView, presenterView, showCreatorView; // Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (require('electron-squirrel-startup')) { - app.quit(); +if (require("electron-squirrel-startup")) { + app.quit(); } protocol.registerSchemesAsPrivileged([ - { - scheme: "media", - privileges: { - // secure: true, - bypassCSP: true, - stream: true, - }, + { + scheme: "media", + privileges: { + secure: true, + bypassCSP: true, + stream: true, + supportFetchAPI: true, + standard: true, }, -]) - -app.disableHardwareAcceleration() - -function parseRangeRequests(text, size) { - const token = text.split("="); - if (token.length !== 2 || token[0] !== "bytes") { - return []; - } - - return token[1] - .split(",") - .map((v) => parseRange(v, size)) - .filter(([start, end]) => !isNaN(start) && !isNaN(end) && start <= end); -} - -const NAN_ARRAY = [NaN, NaN]; - -function parseRange(text, size) { - const token = text.split("-"); - if (token.length !== 2) { - return NAN_ARRAY; - } - - const startText = token[0].trim(); - const endText = token[1].trim(); - - if (startText === "") { - if (endText === "") { - return NAN_ARRAY; - } else { - let start = size - Number(endText); - if (start < 0) { - start = 0; - } + }, +]); - return [start, size - 1]; - } - } else { - if (endText === "") { - return [Number(startText), size - 1]; - } else { - let end = Number(endText); - if (end >= size) { - end = size - 1; - } - - return [Number(startText), end]; - } - } -} +app.disableHardwareAcceleration(); +let canQuit = false; // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.on('ready', () => { - protocol.handle("media", (request) => { - // https://github.com/electron/electron/issues/38749#issuecomment-1681531939 - const fp = path.join(currentProject.basePath, decodeURIComponent(request.url.slice('media://'.length))) - const stats = fs.statSync(fp); - - // console.log(fp, stats) - const headers = new Headers(); - headers.set("Accept-Ranges", "bytes"); - headers.set("Content-Type", "video/mp4"); - - let status = 200; - const rangeText = request.headers.get("range"); - - if (rangeText) { - const ranges = parseRangeRequests(rangeText, stats.size); - - const [start, end] = ranges[0]; - // console.log(rangeText, stats.size, start, end); - headers.set("Content-Length", `${end - start + 1}`); - headers.set("Content-Range", `bytes ${start}-${end}/${stats.size}`); - status = 206; - VidFilestream = fs.createReadStream(fp, {start, end}); - } else { - headers.set("Content-Length", `${stats.size}`); - VidFilestream = fs.createReadStream(fp); - - } - - return new Response(VidFilestream, { - headers, - status, - }); - }) - - showCreatorView = createShowCreatorView() - showCreatorView.on('close', (e) => { - if (currentProject.isOpened) { - let choice = dialog.showMessageBoxSync(showCreatorView, - { - type: 'question', - title: 'Save your Work', - message: 'Make sure to save your work before Quitting \nAre you sure you want to Quit?', - buttons: ['Yes', 'No'], - }) - if (choice === 1) { - e.preventDefault() - } else { - if (currentProject.isOpened) { - VidFilestream?.destroy() - currentProject.closeProject() - } - } - } - }) +app.on("ready", () => { + protocol.handle("media", async (request) => { + const responder = new MediaResponder(request, currentProject); + return await responder.handle(); + + // https://github.com/electron/electron/issues/38749#issuecomment-1681531939 + }); + + showCreatorView = createShowCreatorView(); + showCreatorView.on("close", (e) => { + if (currentProject.isOpened && !canQuit) { + e.preventDefault(); // stop immediate close + showCreatorView.webContents.send("save-before-quit"); + } + }); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. -app.on('window-all-closed', () => { - - if (process.platform !== 'darwin') { - app.quit(); - } +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } }); - -app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - createPresenterView(); - } +app.on("activate", () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createPresenterView(); + } }); ipcMain.handle("file-dialog-open", (e, mode) => { - let filePath - let fileFilters = [{name: "ChoirSlide Files", extensions: ["chs"]}] - if (mode === "o") { - filePath = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), { - properties: ["openFile"], filters: fileFilters - }) - } else if (mode === "s") { - filePath = dialog.showSaveDialogSync(BrowserWindow.getFocusedWindow(), { - properties: ["openFile"], filters: fileFilters - }) - } - return filePath ? filePath : "" - -}) + let filePath; + let fileFilters = [{ name: "ChoirSlide Files", extensions: ["chs", "json"] }]; + if (mode === "o") { + filePath = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), { + properties: ["openFile"], + filters: fileFilters, + }); + } else if (mode === "s") { + filePath = dialog.showSaveDialogSync(BrowserWindow.getFocusedWindow(), { + properties: ["openFile"], + filters: fileFilters, + }); + } + return filePath ? filePath : ""; +}); ipcMain.handle("file-opened", async (e, data) => { - let mainWindow = BrowserWindow.getFocusedWindow().getParentWindow(); - BrowserWindow.getFocusedWindow().destroy() + let mainWindow = BrowserWindow.getFocusedWindow().getParentWindow(); + BrowserWindow.getFocusedWindow().destroy(); + currentProject = new WorkingFile({ ...data }); + if (!data.present) { + await currentProject.editProject(); - if (currentProject.isOpened) { - VidFilestream?.destroy() - mainWindow.webContents.send("slideshow:destroy") - currentProject.closeProject() - } - - currentProject = new WorkingFile(data) - - if (!fs.existsSync(data["filePath"])) { - currentProject.createProject() - } else { - currentProject.openProject() - } - mainWindow.setTitle(`ChoirSlide - ${currentProject.projectName}`) + mainWindow.setTitle(`ChoirSlide - ${currentProject.projectName}`); mainWindow.webContents.send("file-params", currentProject.toObject()); -}) + return; + } + await currentProject.presentProject(); + initPresentationView(); +}); ipcMain.handle("file-save", (e, content) => { - dialog.showMessageBox(BrowserWindow.fromId(e.frameId), { - title: "File Save", - message: "File Is Saving", - type: "info", - }) - - currentProject.saveProject(content).then(() => { - dialog.showMessageBox(BrowserWindow.fromId(e.frameId), { - title: "File Save", - message: "File Saved Successfully", - type: "info" - }) - }).catch(err => { - dialog.showMessageBoxSync({ - title: "File Save", - message: `An error occurred during saving the file \n ${err}`, - type: "info" - }) - }) -}) - -ipcMain.handle("create-thumb", (e, props) => { - const base64Data = props.pic.replace(/^data:image\/png;base64,/, ""); - return fs.writeFileSync(`${currentProject.basePath}/${props.filename}.png`, base64Data, 'base64'); -}) - -ipcMain.handle("copy-video", (e, videoOriginalPath) => { - console.log(videoOriginalPath) - let videoFileName = path.basename(videoOriginalPath) - return fs.copyFileSync(`${videoOriginalPath}`, `${currentProject.basePath}/${videoFileName}`) -}) + dialog.showMessageBox(BrowserWindow.fromId(e.frameId), { + title: "File Save", + message: "File Is Saving", + type: "info", + }); + currentProject.saveProject(content); +}); ipcMain.handle("save-quit", async (e, content) => { - log.info("Save and quit IPC") - /*if (JSON.stringify(workingFile) !== "{}") { - let choice = dialog.showMessageBoxSync(BrowserWindow.fromWebContents(e.sender), - { - type: 'question', - title: 'Save your Work', - message: 'Do you want to save your work before quitting', - buttons: ['Yes', 'No'], - }); - if (choice === 0) { - if (fs.existsSync(workingFile["basePath"])) { - try { - await saveShow(content) - fs.rmSync(path.join(workingFile['directory'], `${projectRandom}.json`)) - fs.rmSync(workingFile["basePath"], {recursive: true, force: true}); - dialog.showMessageBoxSync(BrowserWindow.fromWebContents(e.sender), { - title: "File Save", - message: "File Saved Successfully", - type: "info" - }) - showCreatorView.destroy() - app.quit() - } catch (err) { - dialog.showMessageBoxSync(BrowserWindow.fromWebContents(e.sender), { - title: "File Save", - message: `An error occurred during saving the file \n ${err}`, - type: "error" - }) - } - } else { - showCreatorView.destroy() - app.quit() - } - } else { - showCreatorView.destroy() - app.quit() - } - }*/ -}) + console.log({ e, content }); + return currentProject.closeProject(content); +}); -ipcMain.handle("slideshow:start", (e, content) => { +ipcMain.on("save-done", () => { + canQuit = true; + if (showCreatorView) { + showCreatorView.close(); + } +}); - let choice = dialog.showMessageBoxSync(showCreatorView, - { - type: 'question', - title: 'Save your Work', - message: 'Please make sure that you have saved the show before starting \nAre you sure you want to continue ?', - buttons: ['Yes', 'No'], - }); - if (choice === 0) { - let displays = electron.screen.getAllDisplays() - const externalDisplay = displays.find((display) => { - return display.bounds.x !== 0 || display.bounds.y !== 0 - }) +ipcMain.handle("getSystemFonts", async () => { + const systemFontsScriptPath = getExtraResourcesPath( + "fontlist/getSystemFonts.js" + ); + console.log(systemFontsScriptPath); + return new Promise((resolve, reject) => { + const forked = cp.fork(systemFontsScriptPath); + + forked.on("message", (message) => { + resolve(message); + forked.kill(); + }); + + forked.on("error", (err) => { + reject(err); + }); + + forked.on("exit", (code) => { + if (code !== 0) { + reject(new Error(`getSystemFonts.js exited with code ${code}`)); + } + }); + }); +}); - let data = currentProject.toObject() - if (externalDisplay) { - presenterView = createPresenterView() - presenterView.webContents.once('dom-ready', () => { - presenterView.webContents.send("file-params", data) - }) +ipcMain.on( + "addSlideFiles", + (_, { imgBase64, imgFileName, videoFilePath, videoFileName }) => { + const base64Data = imgBase64.replace(/^data:image\/png;base64,/, ""); + let imgBuffer = Buffer.from(base64Data, "base64"); + currentProject.addVideoSlideFiles({ + imgBuffer, + imgFileName, + videoFilePath, + videoFileName, + }); + } +); + +function initPresentationView() { + let displays = electron.screen.getAllDisplays(); + const externalDisplay = displays.find((display) => { + return display.bounds.x !== 0 || display.bounds.y !== 0; + }); + + let data = currentProject.toObject(); + console.log(data); + if (externalDisplay) { + presenterView = createPresenterView(); + presenterView.webContents.once("dom-ready", () => { + presenterView.webContents.send("file-params", data); + }); + + presentationView = createPresentationView( + presenterView, + externalDisplay.bounds.x, + externalDisplay.bounds.y + ); + presentationView.webContents.once("dom-ready", () => { + presentationView.webContents.send("main:presentation", { + type: "init", + data, + }); + }); + showCreatorView.destroy(); + presenterView.focus(); + } else { + dialog.showMessageBoxSync(showCreatorView, { + type: "error", + title: "No Second Screen Detected", + message: + "Please make sure to connect another screen and the projection mode is set to Extend", + }); + } +} - presenterView.on('close', () => { - currentProject.closeProject() - }) - presentationView = createPresentationView(presenterView, externalDisplay.bounds.x, externalDisplay.bounds.y) - presentationView.webContents.once('dom-ready', () => { - presentationView.webContents.send("main:presentation", {type: "init", data}) - }) - showCreatorView.destroy() - presenterView.focus() - } else { - dialog.showMessageBoxSync(showCreatorView, { - type: 'error', - title: "No Second Screen Detected", - message: "Please make sure to connect another screen and the projection mode is set to Extend" - }) - } - } -}) +ipcMain.handle("slideshow:start", (e, content) => { + let choice = dialog.showMessageBoxSync(showCreatorView, { + type: "question", + title: "Save your Work", + message: + "Please make sure that you have saved the show before starting \nAre you sure you want to continue ?", + buttons: ["Yes", "No"], + }); + if (choice === 0) { + initPresentationView(); + } +}); //presenter:main //main:presentation ipcMain.on("to-presentation", (e, msg) => { - // console.log(msg) - presentationView?.webContents.send("main:presentation", msg) -}) - + // console.log(msg) + presentationView?.webContents.send("main:presentation", msg); +}); diff --git a/src/renderer/ShowCreateView/index.html b/src/renderer/ShowCreateView/index.html index 26d3ae0..0a0c163 100644 --- a/src/renderer/ShowCreateView/index.html +++ b/src/renderer/ShowCreateView/index.html @@ -1,85 +1,410 @@ - - - + + ChoirSlides - - - -
    -
    -
    -
    -
    - -
    -
    + + + + +
    +
    + +
    + +
    +
    +
    +
    +
    + videocam + Video Preview +
    +
    + + + +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + text_fields + Slide Text Editor +
    +
    + + +
    + + + + + +
    + + +
    +
    +
    +
    + +
    +
    +
    - +
    +
    +
    + +
    + add_circle +

    + No slides yet. Click "Add New Slide" to get started. +

    +
    + +
    -
      - -
    +
    + + info + Total slides: 0 + +
    +
    +
    -
    - - - - \ No newline at end of file + + diff --git a/src/renderer/ShowCreateView/js/fileOpen.js b/src/renderer/ShowCreateView/js/fileOpen.js index c7adced..1b28f44 100644 --- a/src/renderer/ShowCreateView/js/fileOpen.js +++ b/src/renderer/ShowCreateView/js/fileOpen.js @@ -1,55 +1,61 @@ -import Slide from "../../js/Classes/SlideClass"; -import ShowCreator from "../../js/Classes/ShowCreatorClass"; +import Slide from "../../js/Classes/Slide"; +import ShowCreator from "../../js/Classes/ShowCreator"; import hotkeys from "hotkeys-js"; -let present +/** + * @type {ShowCreator} + */ +let present; window.file.onFileParams(function (fileParams) { - console.log(fileParams); - let presentation = JSON.parse(fileParams["content"]) - let slides = presentation.map(e => new Slide(e)) - let slidePreviewCanv = document.getElementById("currentSlideThumbCanvas"); - - // comm.toPresentation({type: "init", data: JSON.stringify(fileParams)}) - present = new ShowCreator({ - container: "currentSlideThumbCanvas", - sidebarSlidesContainer: document.getElementById("sidebarSlidesContainer"), - lyricsContainer: document.getElementById("textSlidesList"), - slideTextEditor: document.getElementById("textSlidesEditor"), - width: slidePreviewCanv.clientWidth, - height: slidePreviewCanv.clientHeight, - slides, - basePath: fileParams.basePath, - mode: fileParams.mode, - sepBy: fileParams.sepBy - }) - - // window.onbeforeunload = (e) => { - // e.preventDefault() - // file.saveAndQuit(present.saveShow()) - // - // } - - -}) -hotkeys('delete,ctrl+s', function (event, handler) { - switch (handler.key) { - case 'delete': - present?.removeSlide() - break; - case 'ctrl+s': - file.save(present.saveShow()) + console.log(fileParams); + let presentation = JSON.parse(fileParams["content"]); + let slides = presentation.map((e) => new Slide(e)); + let slidePreviewCanv = document.getElementById("currentSlideThumbCanvas"); + + present = new ShowCreator({ + slides: [...slides], + sidebarSlidesContainer: document.getElementById("sidebarSlidesContainer"), + container: "currentSlideThumbCanvas", + width: slidePreviewCanv.clientWidth, + height: slidePreviewCanv.clientHeight, + splitStrategy: fileParams.mode, + splitDelimiter: fileParams.sepBy, + addSlideBtn: document.querySelector(`#slideAdd`), + removeSlideBtn: document.querySelector(`#slideDelete`), + textEditorField: document.querySelector("textarea"), + fontSelector: document.querySelector("#fontSelector"), + backgroundEnabledBtn: document.querySelector("#backgroundEnabledBtn"), + videoToolbar: document.querySelector("#videoToolbar"), + }); + + window.file.onSaveBeforeQuit(async () => { + console.log("Saving data before quitting..."); + if (await file.saveAndQuit(present.stringifyShow())) { + window.file.saveDone(); } -}) + }); +}); +hotkeys("delete,ctrl+s", function (event, handler) { + switch (handler.key) { + case "delete": + present?.removeSlide(); + break; + case "ctrl+s": + file.save(present.stringifyShow()); + console.log(present.stringifyShow()); + } +}); window.comm.onSlideshowInitialized(() => { - comm.startSlideshow(present.saveShow()) -}) - -window.comm.onSlideshowDestroy(() => { + comm.startSlideshow(present.stringifyShow()); +}); + +/* window.comm.onSlideshowDestroy(() => { + present?.destroyCreator(); +}); +window.onbeforeunload = () => { + console.log("Destroying show") present?.destroyCreator() -}) -// window.onbeforeunload = () => { -// console.log("Destroying show") -// present?.destroyCreator() -// } \ No newline at end of file +} + */ diff --git a/src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..a98aa97 --- /dev/null +++ b/src/renderer/asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/css/_style.scss b/src/renderer/css/_style.scss index 6ce250d..bf2dc6d 100644 --- a/src/renderer/css/_style.scss +++ b/src/renderer/css/_style.scss @@ -1,14 +1,14 @@ @font-face { - font-family: 'Material Symbols Outlined'; + font-family: "Material Symbols Outlined"; font-style: normal; - src: url(../asset/resource/MaterialSymbolsRounded.woff2) format('woff'); + src: url(../asset/resource/MaterialSymbolsRounded.woff2) format("woff"); } .material-symbols-outlined { - font-family: 'Material Symbols Outlined'; + font-family: "Material Symbols Outlined"; font-weight: normal; font-style: normal; - font-size: 1.5em; /* Preferred icon size */ + font-size: 1.5em; /* Preferred icon size */ display: inline-block; line-height: 1; text-transform: none; @@ -19,7 +19,7 @@ } body { - margin: .5% .1%; + margin: 0.5% 0.1%; height: -webkit-fill-available; user-select: none; } @@ -31,11 +31,11 @@ img { #slidePreview { flex-basis: 75%; background: rgb(43, 45, 48); - padding-right: .2%; + padding-right: 0.2%; height: 100vh; #currentSlideThumbContainer { - height: 50%; + // height: 50%; #videoToolBar { width: fit-content; @@ -43,10 +43,9 @@ img { } .currentSlideThumb { - width: auto; - height: 100%; - aspect-ratio: 16/9; - //object-fit: contain; + object-fit: contain; + height: 75%; + aspect-ratio: 16 / 9; border-radius: $slideThumbCornerRadius; } } @@ -72,15 +71,15 @@ img { width: 100%; } -div{ - #textSlidesList{ +div { + #textSlidesList { height: 22rem; } } #textSlidesList > li:hover:not(.active-text-slide) { - color: rgba(255, 255, 255, 0.50); + color: rgba(255, 255, 255, 0.5); } .active-text-slide { color: white; -} \ No newline at end of file +} diff --git a/src/renderer/js/Classes/CanvasRendererClass.js b/src/renderer/js/Classes/CanvasRendererClass.js new file mode 100644 index 0000000..a9d08a7 --- /dev/null +++ b/src/renderer/js/Classes/CanvasRendererClass.js @@ -0,0 +1,343 @@ +import Slide from "./Slide"; +import Konva from "konva"; +import addVideoSvg from "../../asset/resource/video_camera_back_add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg"; + +// TODO: https://chatgpt.com/share/6889bfdd-c3b0-8002-bf5b-9b05270064f0 +// eslint-disable-next-line import/no-webpack-loader-syntax +import videojs from "video.js"; +import Utils from "./Utils"; + +/** + * The Canvas renderer props + * @typedef {Object} presenter_Props + * @property {string} container - slide preview canvas id. + * @property {HTMLDivElement} sidebarSlidesContainer - sidebar container id. + * @property {HTMLDivElement} slideTextEditor - sidebar container id. + * @property {Slide[]} slides - Array of Slide objects. + * @property {number} width - Canvas width. + * @property {number} height - Canvas height. + * @property {string} basePath - The path of the base file and videos. + * @property {string} mode - Separate by words or by delimiter. + * @property {(number|string)} sepBy - Indicates whether the Wisdom component is present. + */ + +class CanvasRenderer extends Konva.Stage { + /** + * @type {[Slide]} + */ + #slides = []; + /** + * @type {number} + * */ + #currentSlide; + + #sideBarSlidesContainer; + #baseLayer; + #textLayer; + #background; + #textBackground; + #videoObj; + #anim; + #filePicker; + #controller; + #textToHeightRatio = 0.125; + + #simpleText; + #padding; + + #createFilePicker() { + let filePicker = document.createElement("input"); + filePicker.type = "file"; + filePicker.accept = "video/*"; + filePicker.addEventListener("change", this.#onVideoFilePicked.bind(this), { + signal: this.#controller.signal, + }); + return filePicker; + } + #createVideoElement() { + let videoElement = document.createElement("video"); + videoElement.autoplay = true; + videoElement.loop = true; + videoElement.controls = true; + videoElement.muted = true; + return videoElement; + } + + pickVideoFile() { + this.#filePicker.click(); + } + + #onImageLayerClicked(e) { + if (this.#currentSlide.videoFileName === undefined) { + this.pickVideoFile(); + } + if (this.#videoObj.paused) { + this.#videoObj.play(); + } else { + this.#videoObj.pause(); + } + } + + async #onVideoFilePicked(e) { + let filePicker = e.target; + let file = filePicker.files.item(0); + let videoFileName = file.name; + let imgFileName = videoFileName.replace(/\.[^/.]+$/, ""); + + try { + let generatedImage = await Utils.createVideoCoverImage(file.path); + const add = await slideFiles.addSlideFiles({ + imgBase64: generatedImage, + videoFilePath: file.path, + imgFileName, + videoFileName, + }); + } catch (err) { + console.error(err); + } + + this.onVideoPicked?.(videoFileName); + } + + /** + * Sets the canvas background to a video if the provided slide contains a video file. + * Updates the video source, sets the background image, and starts video playback and animation. + * If no video file is present in the slide, triggers the canvas video picker. + * + * @private + * @param {Slide} [slide=this.#currentSlide] - The slide object to use for setting the video background. + */ + #setCanvasToVideo(/** Slide*/ slide = this.#currentSlide) { + if (slide.videoFileName !== undefined) { + this.container().style.background = "transparent"; + // this.#videoObj.src = "file://" + this.#basePath + "/" + slide.videoFileName + "." + slide.videoFileFormat + this.#videoObj.src = + "media://" + + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat); + this.#videoObj.muted = slide.isMuted; + + this.#background.setAttrs({ + image: this.#videoObj, + x: 0, + y: 0, + width: this.width(), + height: this.height(), + }); + + this.#videoObj.play(); + this.#anim.start(); + } else { + this.#createCanvasVideoPicker(); + } + } + + #createCanvasVideoPicker() { + let imgdim = this.height() * 0.5; + Konva.Image.fromURL(addVideoSvg, (imageNode) => { + this.#background.setAttrs({ + image: imageNode.image(), + width: imgdim, + height: imgdim, + x: (this.width() - imgdim) / 2, + y: (this.height() - imgdim) / 2, + }); + }); + this.container().style.background = "#000"; + } + + constructor(props) { + super(props); + + this.container().children[0].classList.add("border"); + this.container().children[0].classList.add("border-light-subtle"); + this.#padding = 0.2; + + this.onVideoPicked = props.onVideoPicked; + this.muteVideoBtn = props.muteVideoBtn; + this.onTextDrag = props.onTextDragFn; + this.#controller = new AbortController(); + + this.#videoObj = this.#createVideoElement(); + this.#filePicker = this.#createFilePicker(); + + this.#baseLayer = new Konva.Layer(); + this.add(this.#baseLayer); + + this.#anim = new Konva.Animation(function () { + // do nothing, animation just need to update the layer + }, this.#baseLayer); + + this.#anim.start(); + + /** + * @type {Konva.Image} + */ + this.#background = new Konva.Image({ + x: 0, + y: 0, + width: this.width(), + height: this.height(), + }); + + /* this.#textLayer = new Konva.Group({ + draggable: true, + boundBoxFunc: (oldBox, newBox) => { + // Adopted from https://konvajs.org/docs/sandbox/Limited_Drag_And_Resize.html + // Calculate the actual bounding box of the transformed shape + const box = Utils.getClientRect(newBox); + + // Check if the new box is outside the stage boundaries + const isOut = + box.x < 0 || + box.y < 0 || + box.x + box.width > this.width() || + box.y + box.height > this.height(); + + // If outside boundaries, keep the old box + if (isOut) { + return oldBox; + } + + // If within boundaries, allow the transformation + return newBox; + }, + }); + */ + this.#simpleText = new Konva.Text({ + x: 0, + y: 0, + width: this.width() * 0.5, + text: "", + /* + \u200f The right-to-left mark (RLM) is a non-printing character used in the computerized typesetting of bi-directional + text containing a mix of left-to-right scripts (such as Latin and Cyrillic) and right-to-left scripts + (such as Arabic, Syriac, and Hebrew). + https://en.wikipedia.org/wiki/Right-to-left_mark + https://github.com/konvajs/konva/issues/552 + */ + fontFamily: "Calibri", + fill: "white", + id: "text", + fontSize: this.height() * this.#textToHeightRatio, + align: "center", + fontStyle: "bold", + lineHeight: 1.25, + padding: this.#padding * 10, + }); + + this.#textLayer = new Konva.Label({ + x: 0, + y: 0, + opacity: 1, + draggable: true, + }); + + this.#textBackground = new Konva.Tag({ + fill: "black", + opacity: 0.5, + cornerRadius: 12, + padding: this.#padding * 10, + }); + this.#textLayer.add(this.#textBackground); + + this.#textLayer.add(this.#simpleText); + + this.#baseLayer.add(this.#background); + + this.#baseLayer.add(this.#textLayer); + + console.log(`${this.constructor.name} initialized `); + } + + _attachEventListeners() { + this.#baseLayer.on("mouseover", function (evt) { + var shape = evt.target; + document.body.style.cursor = "pointer"; + }); + this.#baseLayer.on("mouseout", function (evt) { + var shape = evt.target; + document.body.style.cursor = "default"; + }); + + this.#baseLayer.on("click", this.#onImageLayerClicked.bind(this)); + + this.#textLayer.dragBoundFunc((pos) => { + // Clone the group and simulate the new position + const clone = this.#textLayer.clone(); + clone.position(pos); + const box = clone.getClientRect(); + + let newX = pos.x; + let newY = pos.y; + + const stageWidth = this.width(); + const stageHeight = this.height(); + + if (box.x < 0) { + newX = pos.x - box.x; + } + if (box.y < 0) { + newY = pos.y - box.y; + } + if (box.x + box.width > stageWidth) { + newX = pos.x - (box.x + box.width - stageWidth); + } + if (box.y + box.height > stageHeight) { + newY = pos.y - (box.y + box.height - stageHeight); + } + return { x: newX, y: newY }; + }); + this.#textLayer.on("dragend", (e) => { + const lastTextPos = e.target._lastPos; + const relativeTextPos = { + x: lastTextPos.x / this.width(), + y: lastTextPos.y / this.height(), + }; + this.onTextDrag?.(relativeTextPos); + }); + } + + renderSlide(slide) { + this.#currentSlide = slide; + this.#setCanvasToVideo(slide); + } + + changeVideoMuteState(isMuted) { + this.#videoObj.muted = isMuted; + } + + rendertext(text) { + this.#simpleText.text(text); + } + renderTextPosition({ x, y }) { + this.#textLayer.x(x * this.width()); + this.#textLayer.y(y * this.height()); + } + renderTextProps(textProps) { + this.#simpleText.setAttrs({ ...textProps }); + } + renderTextBackground(props) { + if (props) { + this.#textBackground.fill(props.color); + this.#textBackground.opacity(props.opacity); + } else { + this.#textBackground.opacity(0); + } + } + + destroyCreator() { + this.#videoObj.pause(); + this.#videoObj.src = ""; + this.#videoObj.load(); + while ( + this.#sideBarSlidesContainer.firstChild && + this.#sideBarSlidesContainer.removeChild( + this.#sideBarSlidesContainer.firstChild + ) + ); + this.#controller.abort(); + this.destroy(); + } +} + +export default CanvasRenderer; diff --git a/src/renderer/js/Classes/LyricManagerClass.js b/src/renderer/js/Classes/LyricManagerClass.js new file mode 100644 index 0000000..e48ab7f --- /dev/null +++ b/src/renderer/js/Classes/LyricManagerClass.js @@ -0,0 +1,58 @@ +export default class LyricManager { + static STRAT_DELIMETER = "delim"; + static STRAT_WORDS = "words"; + constructor(props) { + this.onFinished = props.onFinishedSlideCallback; + this.onPrevious = props.onPreviousSlideCallback; + this.splitStrategy = props.splitStrategy; + this.splitDelimiter = props.splitDelimiter; + this.currentSlide = null; + this.lyricChunks = []; + this.currentIndex = 0; + } + loadSlide(slide) { + this.currentSlide = slide; + this.lyricChunks = this.splitIntoChunks(slide.text); + this.currentIndex = 0; + } + + splitIntoChunks(text) { + let subtitledText = []; + + if (this.splitStrategy === LyricManager.STRAT_WORDS) { + this.splitDelimiter *= 1; + let textSplit = text.split(" "); + for (let i = 0; i < textSplit.length; i += this.splitDelimiter) { + subtitledText.push( + textSplit.slice(i, i + this.splitDelimiter).join(" ") + ); + } + } else { + subtitledText = text.split(this.splitDelimiter); + } + console.log(subtitledText); + return subtitledText; + } + getCurrentLyric() { + return this.lyricChunks[this.currentIndex] || ""; + } + + next() { + if (this.currentIndex < this.lyricChunks.length - 1) { + this.currentIndex++; + } else { + this.onFinished?.(); // Notify ShowCreator to go to next slide + } + } + + previous() { + if (this.currentIndex === 0) { + this.onPrevious?.(); + } else { + this.currentIndex--; + } + } + reset() { + this.currentIndex = 0; + } +} diff --git a/src/renderer/js/Classes/ShowClass.js b/src/renderer/js/Classes/ShowClass.js index 9274731..38362ee 100644 --- a/src/renderer/js/Classes/ShowClass.js +++ b/src/renderer/js/Classes/ShowClass.js @@ -13,76 +13,73 @@ import ShowPresentationBase from "./ShowPresentationBaseClass"; import Konva from "konva"; - /** * A class for creating presentation * @class Show * @extends ShowPresentationBase */ class Show extends ShowPresentationBase { - - /** - * Creates Video Object for a slide - * @param {Slide} slide - Slide to create its video Element - * @return {HTMLElement} - */ - _createBackground(slide) { - let VideoObj = document.createElement('video'); - VideoObj.src = "media://" + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat) - // VideoObj.src = `${this._basePath}/${slide.videoFileName}.${slide.videoFileFormat}` - VideoObj.muted = true - VideoObj.loop = true - VideoObj.preload = "auto" - return VideoObj + /** + * Creates Video Object for a slide + * @param {Slide} slide - Slide to create its video Element + * @return {HTMLElement} + */ + _createBackground(slide) { + let VideoObj = document.createElement("video"); + VideoObj.src = + "media://" + + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat); + // VideoObj.src = `${this._basePath}/${slide.videoFileName}.${slide.videoFileFormat}` + VideoObj.muted = slide.isMuted; + VideoObj.loop = true; + VideoObj.preload = "auto"; + return VideoObj; + } + + /** + * Create a Presentation. + * @param {presentation_Props} props - presentation props + */ + + constructor(props) { + super(props); + + this._backgroundObjs[0].play(); + + this.anim = new Konva.Animation(function () { + // do nothing, animation just need to update the layer + }, this.baseLayer); + + this.anim.start(); + } + + destroyShow() { + this._backgroundObjs.forEach((element, index) => { + element.pause(); + element.src = ""; + element.load(); + }); + } + + /** + * Changes the slides dynamically the lyrics first then the video + * @param number + * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} + */ + changeSlide(number = 1) { + let { currentTextSlide, currentSlide, isNewSlide } = super.changeSlide( + number + ); + if (isNewSlide) { + if (this.slides[currentSlide].videoFileName !== undefined) { + this._backgroundObjs[currentSlide].play(); + } + if (currentSlide !== this._slides.length - 1) { + this._backgroundObjs[currentSlide + number].pause(); + } } - - - /** - * Create a Presentation. - * @param {presentation_Props} props - presentation props - */ - - constructor(props) { - super(props) - - this._backgroundObjs[0].play() - - - this.anim = new Konva.Animation(function () { - // do nothing, animation just need to update the layer - }, this.baseLayer); - - this.anim.start() - } - - destroyShow() { - this._backgroundObjs.forEach((element, index) => { - element.pause() - element.src = '' - element.load() - - }) - } - - /** - * Changes the slides dynamically the lyrics first then the video - * @param number - * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} - */ - changeSlide(number = 1) { - let {currentTextSlide, currentSlide, isNewSlide} = super.changeSlide(number) - if (isNewSlide) { - if (this.slides[currentSlide].videoFileName !== undefined) { - this._backgroundObjs[currentSlide].play() - } - if (currentSlide !== this._slides.length - 1) { - this._backgroundObjs[currentSlide + number].pause() - } - } - return {currentTextSlide, currentSlide, isNewSlide} - } - - + return { currentTextSlide, currentSlide, isNewSlide }; + } } -export default Show; \ No newline at end of file +export default Show; diff --git a/src/renderer/js/Classes/ShowCreator.js b/src/renderer/js/Classes/ShowCreator.js new file mode 100644 index 0000000..ea453be --- /dev/null +++ b/src/renderer/js/Classes/ShowCreator.js @@ -0,0 +1,190 @@ +import CanvasRenderer from "./CanvasRendererClass"; +import SidebarRenderer from "./SidebarRendererClass"; +import SlideManager from "./SlideManager"; +import VideoToolbar from "./VideoToolbarClass"; +import Slide from "./Slide"; +import LyricManager from "./LyricManagerClass"; +import TextEditorArea from "./TextEditorClass"; + +/** + * Manages the creation and editing of a video slideshow, including slide management, + * sidebar rendering, canvas rendering, and event handling for UI controls. + * + * @class + * @classdesc Handles the main logic for adding, removing, and updating slides, + * as well as synchronizing UI components such as the sidebar and canvas. + * + */ +class ShowCreator { + #addSlideBtn; + #removeSlideBtn; + #textEditorField; + /** + * Creates an instance of ShowCreator. + * @param {Object} props - Configuration properties for ShowCreator. + * @param {Slide[]} props.slides - Initial slides to load. + * @param {HTMLElement} props.sidebarSlidesContainer - Container for sidebar slides. + * @param {string} props.container - Canvas container element. + * @param {number} props.width - Width of the canvas. + * @param {number} props.height - Height of the canvas. + * @param {HTMLElement} props.addSlideBtn - Button to add a new slide. + * @param {HTMLElement} props.removeSlideBtn - Button to remove the current slide. + * @param {HTMLTextAreaElement} props.textEditorField - Text area for editing slide text. + */ + constructor(props) { + this.slides = new SlideManager({ + slides: props.slides, + onSlideChange: this.onSlideChange.bind(this), + }); + this.lyrics = new LyricManager({ + onFinished: () => this.onLyricsSlideFinished(), + onPrevious: () => this.onLyricsSlidePrevious(), + splitStrategy: props.splitStrategy, + splitDelimiter: props.splitDelimiter, + }); + this.sidebar = new SidebarRenderer({ + container: props.sidebarSlidesContainer, + onSlideClickfn: this.onSlideClicked.bind(this), + }); + this.canvas = new CanvasRenderer({ + container: props.container, + width: (props.height * 16) / 9, + height: props.height, + onVideoPicked: this.onVideoPicked.bind(this), + onTextDragFn: this.onTextDrag.bind(this), + }); + + this.videoToolbar = new VideoToolbar({ + container: props.videoToolbar, + onMuteButton: () => this.onMuteButtonClicked(), + }); + this.textEditor = new TextEditorArea({ + textAreaElement: props.textEditorField, + fontSelectorElement: props.fontSelector, + backgroundBtnElemnt: props.backgroundEnabledBtn, + onTextEditedFn: this.onTextEdited.bind(this), + onFontSelectedFn: this.onFontSelected.bind(this), + onBackgroundToggle: this.onBackgroundBtn.bind(this), + }); + + this.#addSlideBtn = props.addSlideBtn; + this.#removeSlideBtn = props.removeSlideBtn; + this.#textEditorField = props.textEditorField; + this.#renderInitialSlides(); + this.#attachEventListeners(); + console.log(`${this.constructor.name} initialized `); + } + + #attachEventListeners() { + // hook into addSlide, removeSlide, textarea input, etc. + this.#addSlideBtn.addEventListener("click", (e) => this.addNewSlide()); + this.#removeSlideBtn.addEventListener("click", (e) => this.removeSlide()); + + this.sidebar._attachEventListeners(); + this.canvas._attachEventListeners(); + this.videoToolbar._attachEventListeners(); + this.textEditor._attachEventListeners(); + } + + onSlideChange() { + console.log(this.slides.currentSlide); + this.canvas.renderTextPosition(this.slides.currentSlide.textPosition); + this.canvas.renderSlide(this.slides.currentSlide); + this.lyrics.loadSlide(this.slides.currentSlide); + this.canvas.rendertext(this.lyrics.getCurrentLyric()); + this.canvas.renderTextProps({ + fontFamily: this.slides.currentSlide.fontFamily, + }); + + this.textEditor.setTextArea(this.slides.currentSlide.text); + this.textEditor.setFontSelector(this.slides.currentSlide.fontFamily); + + this.canvas.renderTextBackground(this.slides.currentSlide.fontBackground); + this.textEditor.renderBackgroundBtn( + !!this.slides.currentSlide.fontBackground.color + ); + + this.videoToolbar.changeMuteButtonIcon(this.slides.currentSlide.isMuted); + } + + onLyricsSlideFinished() { + this.slides.setCurrent(this.slides.currentIndex + 1); + } + onLyricsSlidePrevious() { + this.slides.setCurrent(this.slides.currentIndex - 1); + } + + addNewSlide( + slide = new Slide({ + text: { value: `Slide ${this.slides.allSlides.length + 1}` }, + video: { name: undefined, muted: true }, + }) + ) { + const idx = this.slides.addSlide(slide); + this.sidebar.addSlideElement(slide, idx); + } + + removeSlide() { + const idx = this.slides.removeSlide(); + this.sidebar.removeSlideElement(idx, this.slides.currentIndex); + // this.#textEditorField.value = this.slides.currentSlide.text; + // this.canvas.renderSlide(this.slides.currentSlide); + } + + #renderInitialSlides() { + this.sidebar.clear(); + if (this.slides.allSlides.length === 0) { + this.addNewSlide(); + } else { + this.slides.allSlides.forEach((s, idx) => { + this.sidebar.addSlideElement(s, idx); + + if (idx === this.slides.currentIndex) { + this.onSlideChange(); + } + }); + } + } + + onTextDrag({ x, y }) { + this.slides.updateTextPosition(x, y); + } + onTextEdited(text) { + this.slides.updateSlideText(text); + this.lyrics.loadSlide(this.slides.currentSlide); + this.canvas.rendertext(this.lyrics.getCurrentLyric()); + this.sidebar.rerenderSlideElementText(this.slides.currentIndex, text); + } + + onVideoPicked(filename) { + this.slides.updateSlideVideo(filename); + console.log(this.slides.currentSlide); + this.sidebar.rerenderSlideThumbnail( + this.slides.currentIndex, + this.slides.currentSlide + ); + this.canvas.renderSlide(this.slides.currentSlide); + } + + onSlideClicked(slideNumber) { + this.slides.setCurrent(slideNumber); + } + + onMuteButtonClicked() { + let newMuteState = this.slides.toggleMuteSlide(); + this.videoToolbar.changeMuteButtonIcon(newMuteState); + this.canvas.changeVideoMuteState(newMuteState); + } + onFontSelected(font) { + this.slides.updateTextFont(font); + this.canvas.renderTextProps({ fontFamily: font }); + } + onBackgroundBtn() { + let backgroundProps = this.slides.toggleSlideBackground(); + this.canvas.renderTextBackground(backgroundProps); + } + stringifyShow() { + return JSON.stringify(this.slides); + } +} +export default ShowCreator; diff --git a/src/renderer/js/Classes/ShowCreatorClass.js b/src/renderer/js/Classes/ShowCreatorClass.js deleted file mode 100644 index 2769588..0000000 --- a/src/renderer/js/Classes/ShowCreatorClass.js +++ /dev/null @@ -1,340 +0,0 @@ -import Slide from "./SlideClass"; -import Konva from "konva"; -import addVideoSvg from '../../asset/resource/video-add-svgrepo-com.svg' - - -/** - * The presenter view props - * @typedef {Object} presenter_Props - * @property {string} container - slide preview canvas id. - * @property {HTMLDivElement} sidebarSlidesContainer - sidebar container id. - * @property {HTMLDivElement} slideTextEditor - sidebar container id. - * @property {Slide[]} slides - Array of Slide objects. - * @property {number} width - Canvas width. - * @property {number} height - Canvas height. - * @property {string} basePath - The path of the base file and videos. - * @property {string} mode - Separate by words or by delimiter. - * @property {(number|string)} sepBy - Indicates whether the Wisdom component is present. - */ - -class ShowCreator extends Konva.Stage { - - /** - * @type {[Slide]} - */ - #slides = [] - /** - * @type {number} - * */ - #currentSlide = -1; - #w; - #h; - #basePath; - #sideBarSlidesContainer; - #addSlideBtn; - #baseLayer; - #background; - #slideTextEditor; - #slideTextInput; - #videoObj; - #anim; - #filePicker; - #controller; - #unrenderedSlides = []; - - - #slidesRadioSelector() { - return document.querySelectorAll(`#${this.#sideBarSlidesContainer.id} input`) - }; - - /** - * Creates sidebar preview Object for a slide - * @param {Slide} slide - Slide to create its image Element - * @param {number} slideId Represent slide id in the list - */ - #createSlideSidebarPreview(slide, slideId) { - let slideContainer = document.createElement("li") - - let radioBtn = document.createElement("input"); - radioBtn.setAttribute("type", "radio"); - radioBtn.name = "slides" - radioBtn.id = "s" + slideId; - - slideContainer.appendChild(radioBtn) - - slideContainer.className = "list-group-item list-group-item-action d-flex" - - let LabelContainer = document.createElement("label"); - // LabelContainer.className = "list-group-item"; - LabelContainer.setAttribute('for', 's' + slideId) - - let divInLabel = document.createElement("div"); - divInLabel.className = "slideThumbContainer "; - - - let slideThumbPreview = document.createElement("img"); - slideThumbPreview.className = "slideThumb"; - if (slide.videoFileName === undefined) { - slide.createVideoCoverImage().then(img => { - slideThumbPreview.src = `${img}`; - }) - } else { - slideThumbPreview.src = "file://" + this.#basePath + "/" + slide.videoFileName + "." + slide.videoThumbnailFormat; - } - - - let slidePrevTextContainer = document.createElement("span"); - slidePrevTextContainer.className = "slideText"; - slidePrevTextContainer.innerText = slide.text - slidePrevTextContainer.dataset.slidenumber = slideId; - slidePrevTextContainer.dir = "rtl" - divInLabel.appendChild(slideThumbPreview); - divInLabel.appendChild(slidePrevTextContainer); - LabelContainer.appendChild(divInLabel); - slideContainer.appendChild(LabelContainer); - - - return slideContainer; - } - - #findSelectedSlidePos() { - return [...this.#slidesRadioSelector()].findIndex(el => el.checked) - } - - #onChangeSlideText(e) { - - // change slide text values and sidebar - let currentText = e.target.value.trim() - this.#slides[this.#currentSlide].text = currentText; - this.#sideBarSlidesContainer.children[this.#currentSlide].querySelector("span").innerText = currentText - } - - #onSlideClick(e) { - /** - * https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_delegation - */ - - this.#currentSlide = this.#findSelectedSlidePos() - this.#slideTextInput.value = this.#slides[this.#currentSlide].text - this.#videoObj.src = "file://" + this.#basePath + "/" + this.#slides[this.#currentSlide].videoFileName + "." + this.#slides[this.#currentSlide].videoFileFormat - this.#setCanvasToVideo() - console.log(this.#currentSlide, this.#slides.length); - - } - - #createFilePicker() { - let filePicker = document.createElement("input"); - filePicker.type = "file"; - filePicker.accept = "video/*"; - filePicker.addEventListener("change", this.#onVideoFilePicked.bind(this), {signal: this.#controller.signal}); - return filePicker - } - - #onImageLayerClicked(e) { - if (this.#slides[this.#currentSlide].videoFileName === undefined) { - this.#filePicker.click() - } - if (this.#videoObj.paused) { - this.#videoObj.play(); - } else { - this.#videoObj.pause(); - } - } - - async #onVideoFilePicked(e) { - - let filePicker = e.target - console.log(filePicker.files.item(0)) - let file = filePicker.files.item(0) - let fileName = file.name - console.log(this, fileName) - this.#slides[this.#currentSlide].videoFileName = fileName; - let videoBasePath = file.path.split("\\") - videoBasePath.pop() - videoBasePath = videoBasePath.join("\\") - let currentSlideImg = this.#slides[this.#currentSlide] - let generatedImage = await currentSlideImg.createVideoCoverImage(videoBasePath) - await thumbs.create({pic: generatedImage, filename: currentSlideImg.videoFileName}) - await window.file.copyVideo(file.path) - this.#sideBarSlidesContainer.children[this.#currentSlide].querySelector("img").src = "file://" + this.#basePath + "/" + currentSlideImg.videoFileName + "." + currentSlideImg.videoThumbnailFormat - this.#setCanvasToVideo(currentSlideImg) - } - - #setCanvasToVideo(/** Slide*/slide = this.#slides[this.#currentSlide]) { - if (slide.videoFileName !== undefined) { - this.container().style.background = "transparent" - // this.#videoObj.src = "file://" + this.#basePath + "/" + slide.videoFileName + "." + slide.videoFileFormat - this.#videoObj.src = "media://" + encodeURIComponent(slide.videoFileName + "." + slide.videoFileFormat) - console.log(this.#videoObj) - this.#background.setAttrs({ - image: this.#videoObj, - x: 0, - y: 0, - width: this.width(), - height: this.height(), - }); - - this.#videoObj.play() - this.#anim.start() - - this.#slideTextInput.value = slide.text - } else { - this.#createCanvasVideoPicker() - } - } - - #createCanvasVideoPicker() { - let imgdim = this.height() * .50 - Konva.Image.fromURL(addVideoSvg, (imageNode) => { - this.#background.setAttrs({ - image: imageNode.image(), - width: imgdim, - height: imgdim, - x: (this.width() - imgdim) / 2, - y: (this.height() - imgdim) / 2 - }); - - }); - this.container().style.background = "#000" - } - - constructor(props) { - super(props); - this.#sideBarSlidesContainer = props.sidebarSlidesContainer - this.#slideTextEditor = props.slideTextEditor - this.#slideTextInput = this.#slideTextEditor.querySelector(`textarea`); - this.#addSlideBtn = document.querySelector(`#slideAdd`); - - this.#controller = new AbortController(); - - - this.#basePath = props.basePath - this.#unrenderedSlides = props.slides - - console.log(this.#slides) - - this.#videoObj = document.createElement("video"); - this.#videoObj.autoplay = true; - this.#videoObj.loop = true - this.#videoObj.muted = true - - - this.#filePicker = this.#createFilePicker() - - - this.#baseLayer = new Konva.Layer({}); - this.add(this.#baseLayer) - - - this.#addSlideBtn.addEventListener("click", () => { - this.addNewSlide() - }, {signal: this.#controller.signal}) - this.#sideBarSlidesContainer.addEventListener("change", this.#onSlideClick.bind(this), {signal: this.#controller.signal}) - this.#slideTextInput.addEventListener("input", this.#onChangeSlideText.bind(this), {signal: this.#controller.signal}) - - this.#anim = new Konva.Animation(function () { - // do nothing, animation just need to update the layer - }, this.#baseLayer); - - this.#anim.start() - - this.#baseLayer.on('mouseover', function (evt) { - var shape = evt.target; - document.body.style.cursor = 'pointer'; - - }); - this.#baseLayer.on('mouseout', function (evt) { - var shape = evt.target; - document.body.style.cursor = 'default'; - }); - - this.#baseLayer.on('click', this.#onImageLayerClicked.bind(this)) - - /** - * - * @type {Konva.Image} - */ - this.#background = new Konva.Image({ - x: 0, y: 0, width: this.width(), height: this.height(), - }); - - this.#baseLayer.add(this.#background) - - this.slideNumber = 0 - - // mapping slides must be at end - this.#unrenderedSlides.forEach(slide => this.addNewSlide(slide)) - - console.log(this.#slides) - if (this.#slides.length === 0) { - this.addNewSlide() - } - } - - - /** - * Adds New slide to the show - * - */ - addNewSlide(newSlide = new Slide({ - text: this.slideNumber + " Please add slide text", videoFileName: undefined - })) { - - - let itemToInsertBefore = this.#sideBarSlidesContainer.children[this.#currentSlide + 1] - this.#currentSlide += 1 - this.#sideBarSlidesContainer.insertBefore(this.#createSlideSidebarPreview(newSlide, ++this.slideNumber), itemToInsertBefore) - document.querySelectorAll("#sidebarSlidesContainer input")[this.#currentSlide].checked = true - - let scrollBehaviour = {behavior: "smooth", block: "start"} - this.#sideBarSlidesContainer.children[this.#currentSlide].scrollIntoView(scrollBehaviour) - this.#slides.splice(this.#currentSlide, 0, newSlide) - - - if (!newSlide.videoFileName) { - this.#slideTextInput.value = "" - this.#createCanvasVideoPicker() - } else { - this.#setCanvasToVideo(this.#slides[this.#currentSlide]) - } - - console.log(this.#currentSlide,this.#slides) - } - - - /** - * Removes slide from the show - * - */ - removeSlide() { - if (this.#currentSlide > -1) { - this.#slides.splice(this.#currentSlide, 1) - let slideElementToRemove = this.#sideBarSlidesContainer.children[this.#currentSlide] - this.#sideBarSlidesContainer.removeChild(slideElementToRemove) - this.#currentSlide -= 1 - - // console.log(this.#currentSlide, this.#slides.length) - if (this.#currentSlide > -1) { - this.#slidesRadioSelector()[this.#currentSlide].checked = true - } - this.#setCanvasToVideo(this.#slides[this.#currentSlide]) - - //TODO: Delete video and Thumbnail from folder - } - } - - saveShow() { - return JSON.stringify(this.#slides) - } - - destroyCreator() { - this.#videoObj.pause() - this.#videoObj.src = '' - this.#videoObj.load() - while (this.#sideBarSlidesContainer.firstChild && this.#sideBarSlidesContainer.removeChild(this.#sideBarSlidesContainer.firstChild)) ; - this.#controller.abort() - this.destroy() - } -} - -export default ShowCreator; \ No newline at end of file diff --git a/src/renderer/js/Classes/ShowPresentationBaseClass.js b/src/renderer/js/Classes/ShowPresentationBaseClass.js index 5c4707e..a0706e1 100644 --- a/src/renderer/js/Classes/ShowPresentationBaseClass.js +++ b/src/renderer/js/Classes/ShowPresentationBaseClass.js @@ -1,192 +1,233 @@ import Konva from "konva"; class ShowPresentationBase extends Konva.Stage { - #w; - #h; - #textToHeightRatio = 0.125 - #textToSpacingRatio = .65 - _basePath; - #simpleText; - #background; - #textSlides; - #currentSlide - #currentTextSlide; - _slides - #textLayer; - #textBG; - #padding = 20; - _backgroundObjs; - _mode; - _sepBy; - static WORDS = "words"; - static DELIMITER = "delimiter"; - - get slides() { - return this._slides; - } - - get basePath() { - return this._basePath; - } - - get mode() { - return this._mode; - } - - get sepBy() { - return this._sepBy; - } - - /** - * Creates Video Object for a slide - * @param {Slide} slide - Slide to create its video Element - * @return {HTMLElement} - */ - _createBackground(slide) { - return new Image(); - } - - /** - * Create video objects for caching. - * @param {number} numOfVideos - Number of videos to be cached after the current video - */ - _cacheBG(numOfVideos = 3) { - return this._slides.slice(this.#currentSlide + 2, this.#currentSlide + 2 + numOfVideos).map(this._createBackground.bind(this)) - } - - /** - * Create a Presentation. - * @param {presentation_Props} props - presentation props - */ - constructor(props) { - super(props); - this._slides = props.slides - this.#w = props.width - this.#h = props.height - this._basePath = props.basePath - this._mode = props.mode - this._sepBy = props.sepBy - this.#currentSlide = 0 - this.#currentTextSlide = 0 - this.#textSlides = this._slides[this.#currentSlide].splitText( - this._mode, - this._sepBy - ) - this._backgroundObjs = this._slides.slice(0, 3).map(this._createBackground.bind(this)) - console.log(this._backgroundObjs) - - this.baseLayer = new Konva.Layer(); - this.#textLayer = new Konva.Layer(); - - this.#background = new Konva.Image({ - image: this._backgroundObjs[0], - x: 0, - y: 0, - width: this.width(), - height: this.height(), - }); - - console.log((this.height() - 225) / this.height(),) - this.#simpleText = new Konva.Text({ - x: 0, - y: this.height() * this.#textToSpacingRatio, - text: `${this.#textSlides[this.#currentTextSlide]}\u202e`, - /* - \u200f The right-to-left mark (RLM) is a non-printing character used in the computerized typesetting of bi-directional - text containing a mix of left-to-right scripts (such as Latin and Cyrillic) and right-to-left scripts - (such as Arabic, Syriac, and Hebrew). - https://en.wikipedia.org/wiki/Right-to-left_mark - https://github.com/konvajs/konva/issues/552 - */ - fontSize: this.height() * this.#textToHeightRatio, - fontFamily: 'Calibri', - fill: 'white', - id: "text", - draggable: false, - width: this.width(), - align: "center", - cornerRadius: 20, - fontStyle: "bold", - lineHeight:1.25 - }); - - this.#textBG = new Konva.Rect({ - // x: this.#w / 2 - this.#simpleText.getTextWidth() / 2 - this.#padding / 2, - x: this.#simpleText.getClientRect().x - this.#padding / 2, - y: this.#simpleText.getClientRect().y - this.#padding / 2, - height: this.#simpleText.getClientRect().height + this.#padding, - width: this.#simpleText.getTextWidth() + this.#padding, - cornerRadius: 20, - fill: "#000", - opacity: .5 - }) - - this.baseLayer.add(this.#background); - - this.#textLayer.getCanvas()._canvas.setAttribute("dir","rtl") - this.#textLayer.add(this.#textBG) - this.#textLayer.add(this.#simpleText) - - this.add(this.baseLayer); - this.add(this.#textLayer) - - this.baseLayer.draw(); - } - - /** - * Changes the slides dynamically the lyrics first then the video - * @param number - * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} - */ - changeSlide(number = 1) { - this.#currentTextSlide += number - let isNewSlide = false - if (this.#currentTextSlide < this.#textSlides.length && this.#currentTextSlide >= 0) { - this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`) - } else if (this.#currentTextSlide === this.#textSlides.length && this.#currentSlide !== this._slides.length - 1) { - this.#currentTextSlide = 0 - this.#currentSlide++ - this.#textSlides = this._slides[this.#currentSlide].splitText( - this._mode, - this._sepBy - ) - this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}\u202e`) - isNewSlide = true - - if (this.#currentSlide === this._backgroundObjs.length - 2) { - this._backgroundObjs.push(...this._cacheBG()) - console.log(this._backgroundObjs) - } - this.#background.setAttr("image", this._backgroundObjs[this.#currentSlide]) - } else if (this.#currentTextSlide < 0 && this.#currentSlide !== 0) { - this.#currentSlide-- - this.#textSlides = this._slides[this.#currentSlide].splitText( - this._mode, - this._sepBy - ) - this.#currentTextSlide = this.#textSlides.length + this.#currentTextSlide - this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`) - isNewSlide = true - this.#background.setAttr("image", this._backgroundObjs[this.#currentSlide]) - } else if (this.#currentTextSlide < 0 && this.#currentSlide === 0) { - this.#currentTextSlide = 0 - } else { - this.#currentTextSlide = this.#textSlides.length - 1 - } - - this.#textBG.setAttrs({ - x: this.#w / 2 - this.#simpleText.getTextWidth() / 2 - this.#padding / 2, - y: this.#simpleText.getClientRect().y - this.#padding / 2, - height: this.#simpleText.getClientRect().height + this.#padding, - width: this.#simpleText.getTextWidth() + this.#padding, - }) - console.log(`Text ${this.#currentTextSlide + 1} of ${this.#textSlides.length},`, - `Slide ${this.#currentSlide + 1} of ${this._slides.length},\n`, - `Text Height: ${this.#simpleText.getClientRect().y - this.#padding / 2}`, - `compare to height: ${+this.#simpleText.getClientRect().y - this.#padding / 2}`) - return {currentTextSlide: this.#currentTextSlide, currentSlide: this.#currentSlide, isNewSlide} + #w; + #h; + #textToHeightRatio = 0.125; + #textToSpacingRatio = 0.65; + _basePath; + #simpleText; + #background; + #textSlides; + #currentSlide; + #currentTextSlide; + _slides; + #textLayer; + #textBG; + #textBackground; + #padding = 0.2; + _backgroundObjs; + _mode; + _sepBy; + static WORDS = "words"; + static DELIMITER = "delimiter"; + + get slides() { + return this._slides; + } + + get basePath() { + return this._basePath; + } + + get mode() { + return this._mode; + } + + get sepBy() { + return this._sepBy; + } + + /** + * Creates Video Object for a slide + * @param {Slide} slide - Slide to create its video Element + * @return {HTMLElement} + */ + _createBackground(slide) { + return new Image(); + } + + /** + * Create video objects for caching. + * @param {number} numOfVideos - Number of videos to be cached after the current video + */ + _cacheBG(numOfVideos = 3) { + return this._slides + .slice(this.#currentSlide + 2, this.#currentSlide + 2 + numOfVideos) + .map(this._createBackground.bind(this)); + } + + /** + * Create a Presentation. + * @param {presentation_Props} props - presentation props + */ + constructor(props) { + super(props); + this._slides = props.slides; + this.#w = props.width; + this.#h = props.height; + this._basePath = props.basePath; + this._mode = props.mode; + this._sepBy = props.sepBy; + this.#currentSlide = 0; + this.#currentTextSlide = 0; + this.#textSlides = this._slides[this.#currentSlide].splitText( + this._mode, + this._sepBy + ); + this._backgroundObjs = this._slides + .slice(0, 3) + .map(this._createBackground.bind(this)); + console.log(this._backgroundObjs); + + this.baseLayer = new Konva.Layer(); + + this.#background = new Konva.Image({ + image: this._backgroundObjs[0], + x: 0, + y: 0, + width: this.width(), + height: this.height(), + }); + + this.#simpleText = new Konva.Text({ + x: 0, + y: 0, + width: this.width() * 0.5, + text: this.#textSlides[this.#currentTextSlide] + "\u202e", + /* + \u200f The right-to-left mark (RLM) is a non-printing character used in the computerized typesetting of bi-directional + text containing a mix of left-to-right scripts (such as Latin and Cyrillic) and right-to-left scripts + (such as Arabic, Syriac, and Hebrew). + https://en.wikipedia.org/wiki/Right-to-left_mark + https://github.com/konvajs/konva/issues/552 + */ + fontFamily: this.slides[this.#currentSlide].fontFamily, + fill: "white", + id: "text", + fontSize: this.height() * this.#textToHeightRatio, + align: "center", + fontStyle: "bold", + lineHeight: 1.25, + padding: this.#padding * 10, + }); + + this.#textLayer = new Konva.Label({ + x: 0, + y: 0, + opacity: 1, + draggable: false, + }); + + this.#textBackground = new Konva.Tag({ + fill: "black", + opacity: this.slides[this.#currentSlide].fontBackground ? 0.5 : 0, + cornerRadius: 12, + padding: this.#padding * 10, + }); + + this.#textLayer.add(this.#textBackground); + + this.#textLayer.add(this.#simpleText); + + this.baseLayer.add(this.#background); + this.baseLayer.add(this.#textLayer); + this.add(this.baseLayer); + + this.baseLayer.draw(); + } + + /** + * Changes the slides dynamically the lyrics first then the video + * @param number + * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} + */ + changeSlide(number = 1) { + this.#currentTextSlide += number; + let isNewSlide = false; + if ( + this.#currentTextSlide < this.#textSlides.length && + this.#currentTextSlide >= 0 + ) { + this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`); + } else if ( + this.#currentTextSlide === this.#textSlides.length && + this.#currentSlide !== this._slides.length - 1 + ) { + this.#currentTextSlide = 0; + this.#currentSlide++; + this.#textSlides = this._slides[this.#currentSlide].splitText( + this._mode, + this._sepBy + ); + this.#simpleText.text( + `${this.#textSlides[this.#currentTextSlide]}\u202e` + ); + this.#simpleText.fontFamily(this._slides[this.#currentSlide].fontFamily); + this.#textLayer.x( + this.width() * this._slides[this.#currentSlide].textPosition.x + ); + this.#textLayer.y( + this.height() * this._slides[this.#currentSlide].textPosition.y + ); + if (this._slides[this.#currentSlide].fontBackground) { + this.#textBackground.opacity(0.5); + this.#textBackground.fill("black"); + } else { + this.#textBackground.opacity(0); + } + isNewSlide = true; + + if (this.#currentSlide === this._backgroundObjs.length - 2) { + this._backgroundObjs.push(...this._cacheBG()); + console.log(this._backgroundObjs); + } + this.#background.setAttr( + "image", + this._backgroundObjs[this.#currentSlide] + ); + } else if (this.#currentTextSlide < 0 && this.#currentSlide !== 0) { + this.#currentSlide--; + this.#textSlides = this._slides[this.#currentSlide].splitText( + this._mode, + this._sepBy + ); + this.#currentTextSlide = this.#textSlides.length + this.#currentTextSlide; + this.#simpleText.text(`${this.#textSlides[this.#currentTextSlide]}`); + this.#simpleText.fontFamily(this._slides[this.#currentSlide].fontFamily); + this.#textLayer.x(this._slides[this.#currentSlide].textPosition.x); + this.#textLayer.y(this._slides[this.#currentSlide].textPosition.y); + if (this._slides[this.#currentSlide].fontBackground) { + this.#textBackground.opacity(0.5); + this.#textBackground.fill("black"); + } else { + this.#textBackground.opacity(0); + } + isNewSlide = true; + this.#background.setAttr( + "image", + this._backgroundObjs[this.#currentSlide] + ); + } else if (this.#currentTextSlide < 0 && this.#currentSlide === 0) { + this.#currentTextSlide = 0; + } else { + this.#currentTextSlide = this.#textSlides.length - 1; } + console.log( + `Text ${this.#currentTextSlide + 1} of ${this.#textSlides.length},`, + `Slide ${this.#currentSlide + 1} of ${this._slides.length},\n`, + `Text Height: ${this.#simpleText.getClientRect().y - this.#padding / 2}`, + `compare to height: ${ + +this.#simpleText.getClientRect().y - this.#padding / 2 + }` + ); + return { + currentTextSlide: this.#currentTextSlide, + currentSlide: this.#currentSlide, + isNewSlide, + }; + } } -export default ShowPresentationBase \ No newline at end of file +export default ShowPresentationBase; diff --git a/src/renderer/js/Classes/ShowPresenterViewClass.js b/src/renderer/js/Classes/ShowPresenterViewClass.js index d753cfa..35082c8 100644 --- a/src/renderer/js/Classes/ShowPresenterViewClass.js +++ b/src/renderer/js/Classes/ShowPresenterViewClass.js @@ -20,116 +20,134 @@ import ShowPresentationBase from "./ShowPresentationBaseClass"; * @extends ShowPresentationBase */ class ShowPresenterView extends ShowPresentationBase { - - #lyricsContainer; - #sideBarSlidesContainer - - /** - * Creates Image Object for a slide - * @param {Slide} slide - Slide to create its image Element - * @return {HTMLElement} - */ - _createBackground(slide) { - let ImgObj = document.createElement('img'); - // VideoObj.src = `media-loader://${encodeURIComponent(`${this.#basePath}/${slide.videoFile}`)}` - ImgObj.src = `file://${this.basePath}/${slide.videoFileName}.${slide.videoThumbnailFormat}` - return ImgObj - } - - /** - * Creates sidebar preview Object for a slide - * @param {Slide} slide - Slide to create its image Element - */ - #createSlideSidebarPreview(slide) { - let slideContainer = document.createElement("li"); - slideContainer.className = "list-group-item list-group-item-action d-box" - - let divContainer = document.createElement("div"); - divContainer.className = "slideThumbContainer"; - - let slideThumbPreview = document.createElement("img"); - slideThumbPreview.className = "slideThumb"; - slideThumbPreview.src = `file://${this.basePath}/${slide.videoFileName}.${slide.videoThumbnailFormat}`; - let slidePrevTextContainer = document.createElement("span"); - slidePrevTextContainer.className = "slideText"; - slidePrevTextContainer.innerText = slide.text - slidePrevTextContainer.dir = "rtl" - - divContainer.appendChild(slideThumbPreview); - divContainer.appendChild(slidePrevTextContainer); - - slideContainer.appendChild(divContainer); - - return slideContainer; - } - - /** - * Creates sidebar preview Object for a slide - * @param {Slide} slide - * @return {HTMLLIElement[]} - */ - #createLyricsPreview(slide) { - let lyricsLines = slide.splitText(this._mode, this._sepBy); - return lyricsLines.map((line, i) => { - //
  • ها بطيب
  • - let liEl = document.createElement("li"); - liEl.textContent = line - liEl.dataset.textslidenumber = i - liEl.dir = "rtl" - return liEl - }); - } - - /** - * Create a Presenter View. - * @param {presenter_Props} props - presentation props - */ - constructor(props) { - super(props); - this.#sideBarSlidesContainer = props.sidebarSlidesContainer - this.#lyricsContainer = props.lyricsContainer - - - let addSlideBtn = document.querySelector(`#${this.#sideBarSlidesContainer.id}>:first-child`); - - this._slides.forEach(slide => { - this.#sideBarSlidesContainer.insertBefore(this.#createSlideSidebarPreview(slide), addSlideBtn) - }) - - this.#lyricsContainer.replaceChildren(...this.#createLyricsPreview(this._slides[0])) - - this.#sideBarSlidesContainer.children[0].classList.add("active"); - this.#lyricsContainer.children[0].classList.add("active-text-slide"); - } - - /** - * Changes the slides dynamically the lyrics first then the video - * @param number - * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} - */ - - changeSlide(number = 1) { - let scrollBehaviour = {behavior: "smooth", block: "center"} - let {currentSlide, currentTextSlide, isNewSlide} = super.changeSlide(number); - - if (isNewSlide) { - let currentActive = this.#sideBarSlidesContainer.children[currentSlide] - this.#lyricsContainer.replaceChildren(...this.#createLyricsPreview(this._slides[currentSlide])) - this.#lyricsContainer.children[currentTextSlide].classList.add("active-text-slide"); - currentActive.classList.add("active"); - currentActive.scrollIntoView(scrollBehaviour) - this.#sideBarSlidesContainer.children[currentSlide - number].classList.remove("active"); - } else { - this.#lyricsContainer.children[currentTextSlide].classList.add("active-text-slide"); - this.#lyricsContainer.children[currentTextSlide].scrollIntoView(scrollBehaviour) - if (this.#lyricsContainer.children.length > 1) { - this.#lyricsContainer.children[currentTextSlide - number].classList.remove("active-text-slide"); - } - } - - return {currentSlide, currentTextSlide, isNewSlide} + #lyricsContainer; + #sideBarSlidesContainer; + + /** + * Creates Image Object for a slide + * @param {Slide} slide - Slide to create its image Element + * @return {HTMLElement} + */ + _createBackground(slide) { + let ImgObj = document.createElement("img"); + // VideoObj.src = `media-loader://${encodeURIComponent(`${this.#basePath}/${slide.videoFile}`)}` + ImgObj.src = `media://${slide.videoFileName}.${slide.videoThumbnailFormat}`; + return ImgObj; + } + + /** + * Creates sidebar preview Object for a slide + * @param {Slide} slide - Slide to create its image Element + */ + #createSlideSidebarPreview(slide) { + let slideContainer = document.createElement("li"); + slideContainer.className = "list-group-item list-group-item-action d-box"; + + let divContainer = document.createElement("div"); + divContainer.className = "slideThumbContainer"; + + let slideThumbPreview = document.createElement("img"); + slideThumbPreview.className = "slideThumb"; + slideThumbPreview.src = `media://${slide.videoFileName}.${slide.videoThumbnailFormat}`; + let slidePrevTextContainer = document.createElement("span"); + slidePrevTextContainer.className = "slideText"; + slidePrevTextContainer.innerText = slide.text; + slidePrevTextContainer.dir = "rtl"; + + divContainer.appendChild(slideThumbPreview); + divContainer.appendChild(slidePrevTextContainer); + + slideContainer.appendChild(divContainer); + + return slideContainer; + } + + /** + * Creates sidebar preview Object for a slide + * @param {Slide} slide + * @return {HTMLLIElement[]} + */ + #createLyricsPreview(slide) { + let lyricsLines = slide.splitText(this._mode, this._sepBy); + return lyricsLines.map((line, i) => { + //
  • ها بطيب
  • + let liEl = document.createElement("li"); + liEl.textContent = line; + liEl.dataset.textslidenumber = i; + liEl.dir = "rtl"; + return liEl; + }); + } + + /** + * Create a Presenter View. + * @param {presenter_Props} props - presentation props + */ + constructor(props) { + super(props); + this.#sideBarSlidesContainer = props.sidebarSlidesContainer; + this.#lyricsContainer = props.lyricsContainer; + + let addSlideBtn = document.querySelector( + `#${this.#sideBarSlidesContainer.id}>:first-child` + ); + + this._slides.forEach((slide) => { + this.#sideBarSlidesContainer.insertBefore( + this.#createSlideSidebarPreview(slide), + addSlideBtn + ); + }); + + this.#lyricsContainer.replaceChildren( + ...this.#createLyricsPreview(this._slides[0]) + ); + + this.#sideBarSlidesContainer.children[0].classList.add("active"); + this.#lyricsContainer.children[0].classList.add("active-text-slide"); + } + + /** + * Changes the slides dynamically the lyrics first then the video + * @param number + * @return {{currentTextSlide:number, isNewSlide: boolean, currentSlide:number}} + */ + + changeSlide(number = 1) { + let scrollBehaviour = { behavior: "smooth", block: "center" }; + let { currentSlide, currentTextSlide, isNewSlide } = super.changeSlide( + number + ); + + if (isNewSlide) { + let currentActive = this.#sideBarSlidesContainer.children[currentSlide]; + this.#lyricsContainer.replaceChildren( + ...this.#createLyricsPreview(this._slides[currentSlide]) + ); + this.#lyricsContainer.children[currentTextSlide].classList.add( + "active-text-slide" + ); + currentActive.classList.add("active"); + currentActive.scrollIntoView(scrollBehaviour); + this.#sideBarSlidesContainer.children[ + currentSlide - number + ].classList.remove("active"); + } else { + this.#lyricsContainer.children[currentTextSlide].classList.add( + "active-text-slide" + ); + this.#lyricsContainer.children[currentTextSlide].scrollIntoView( + scrollBehaviour + ); + if (this.#lyricsContainer.children.length > 1) { + this.#lyricsContainer.children[ + currentTextSlide - number + ].classList.remove("active-text-slide"); + } } + return { currentSlide, currentTextSlide, isNewSlide }; + } } -export default ShowPresenterView; \ No newline at end of file +export default ShowPresenterView; diff --git a/src/renderer/js/Classes/SidebarRendererClass.js b/src/renderer/js/Classes/SidebarRendererClass.js new file mode 100644 index 0000000..23aa894 --- /dev/null +++ b/src/renderer/js/Classes/SidebarRendererClass.js @@ -0,0 +1,119 @@ +class SidebarRenderer { + /** + * Creates an instance of SidebarRenderer. + * @param {HTMLElement} container - The DOM element that will contain the sidebar. + */ + constructor(props = { container: undefined, onSlideClickfn: undefined }) { + this.container = props.container; + this.onSlideClick = props.onSlideClickfn; + console.log(`${this.constructor.name} initialized `); + } + + _attachEventListeners() { + this.container.addEventListener("change", (e) => { + this.onSlideClick?.(e.target.dataset.slideId); + }); + } + + #createSlideElement(slide, index) { + const label = document.createElement("label"); + label.className = "slide-item"; + label.slideId = index; + + const radio = document.createElement("input"); + radio.type = "radio"; + radio.name = "slides"; + radio.id = "s" + index; + radio.className = "visually-hidden"; + radio.dataset.slideId = index; + + const preview = document.createElement("div"); + preview.className = "slide-preview ratio-16x9 w-100"; + preview.style.height = "10rem"; + preview.dataset.slideId = index; + + const icon = document.createElement("span"); + icon.className = "material-symbols-outlined"; + icon.innerText = "hide_image"; + preview.appendChild(icon); + + const num = document.createElement("span"); + num.className = "slide-number"; + num.innerText = index; + preview.appendChild(num); + + const textPreview = document.createElement("div"); + textPreview.className = "slide-text-preview"; + textPreview.innerHTML = slide.text || "Empty slide"; + textPreview.dataset.slideId = index; + + const content = document.createElement("div"); + content.className = "slide-content"; + content.appendChild(textPreview); + + label.appendChild(radio); + label.appendChild(preview); + label.appendChild(content); + + return label; + } + + #insertSlideElement(slideEl, index) { + const beforeEl = this.container.children[index]; + this.container.insertBefore(slideEl, beforeEl || null); + slideEl.querySelector("input").checked = true; + this.rerenderAllSlideNumbers(); + slideEl.scrollIntoView({ behavior: "smooth", block: "start" }); + } + + addSlideElement(slide, index) { + const slideEl = this.#createSlideElement(slide, index); + this.#insertSlideElement(slideEl, index); + if (slide.videoFileName) { + this.rerenderSlideThumbnail(index, slide); + } + } + + removeSlideElement(index, activeSlideIndex) { + console.log("Sidebar before removal", [...this.container.children]); + this.container.removeChild(this.container.children[index]); + this.rerenderAllSlideNumbers(); // ✅ update after removal + this.container.children[activeSlideIndex].querySelector( + "input" + ).checked = true; + } + + rerenderSlideElementText(index, text) { + console.log(this.container.children[index]); + this.container.children[index].querySelector( + ".slide-text-preview" + ).innerText = text; + } + + rerenderSlideThumbnail(index, slide) { + this.container.children[index].querySelector( + ".slide-preview" + ).style.backgroundImage = `url('media://${slide.videoFileName}.${slide.videoThumbnailFormat}')`; + this.container.children[index].querySelector( + ".material-symbols-outlined" + ).innerText = ``; + } + + rerenderAllSlideNumbers() { + Array.from(this.container.children).forEach((slideEl, i) => { + const slidePreview = slideEl.querySelector(".slide-preview"); + if (slidePreview) slidePreview.dataset.slideId = i; + const slideTextPreview = slideEl.querySelector(".slide-text-preview"); + if (slideTextPreview) slideTextPreview.dataset.slideId = i; + const radioBtn = slideEl.querySelector("input"); + if (radioBtn) radioBtn.dataset.slideId = i; + const numberSpan = slideEl.querySelector(".slide-number"); + if (numberSpan) numberSpan.innerText = i + 1; + }); + } + + clear() { + this.container.innerHTML = ""; + } +} +export default SidebarRenderer; diff --git a/src/renderer/js/Classes/Slide.ts b/src/renderer/js/Classes/Slide.ts new file mode 100644 index 0000000..83144be --- /dev/null +++ b/src/renderer/js/Classes/Slide.ts @@ -0,0 +1,188 @@ +interface FontBackground { + color: string; + opacity: number; +} + +interface Font { + family?: string; + bold?: boolean; + textToHeightRatio?: number; + background: FontBackground | false; +} + +interface SlideJSON { + video: { + name?: string; + format?: string; + muted?: boolean; + }; + text?: { + x: number; + y: number; + font: Font; + value: string; + }; + thumbnail?: { + format: string; + }; +} + +class Slide { + private _text: string; + private _textX: number; + private _textY: number; + private _loop: boolean; + private _muted: boolean; + + private _fontFamily: string; + private _fontBold: boolean; + private _fontTextToHeightRatio: number; + private _fontBackground: FontBackground | false; + + private _videoThumbnailFormat: string; + private _videoFileName: string; + private _videoFileFormat: string; + + constructor( + props: SlideJSON = { + video: { name: undefined, muted: true, format: "mp4" }, + } + ) { + this._videoFileName = props?.video?.name?.replace(/\.[^/.]+$/, ""); + this._videoFileFormat = props?.video?.format || "mp4"; + + this._videoThumbnailFormat = props?.thumbnail?.format || "png"; + + this._text = props?.text?.value; + this._textX = props?.text?.x || 0; + this._textY = props?.text?.y || 0; + + this._muted = props.video.muted; + + this._fontFamily = props?.text?.font?.family || "Calibri"; + this._fontBold = props?.text?.font?.bold || true; + this._fontTextToHeightRatio = props?.text?.font?.textToHeightRatio || 0.125; + + this._fontBackground = props?.text?.font?.background || { + color: "black", + opacity: 0.5, + }; + } + + get text() { + return this._text; + } + get fontBackground() { + return this._fontBackground; + } + get fontFamily() { + return this._fontFamily; + } + get textPosition() { + return { x: this._textX, y: this._textY }; + } + /** + * Sets the position of the text overlay on the slide. + * + * @param prop - An object containing the `x` and `y` coordinates for the text position. + * @property prop.x - The horizontal position of the text. + * @property prop.y - The vertical position of the text. + */ + setTextPosition(prop: { x: number; y: number }) { + this._textX = prop.x; + this._textY = prop.y; + } + + get videoThumbnailFormat(): string { + return this._videoThumbnailFormat; + } + get videoFileFormat(): string { + return this._videoFileFormat; + } + get videoFileName(): string { + return this._videoFileName; + } + get loop(): boolean { + return this._loop; + } + + get isMuted(): boolean { + return this._muted; + } + + toggleMuted(): Boolean { + this._muted = !this._muted; + return this._muted; + } + toggleBackground(): FontBackground | false { + if (this._fontBackground) { + this._fontBackground = false; + return this._fontBackground; + } + this._fontBackground = { + color: "black", + opacity: 0.5, + }; + return this._fontBackground; + } + + setText(text: string): void { + this._text = text; + } + setFontName(name: string): void { + this._fontFamily = name; + } + setVideoFileName(filename: string) { + this._videoFileName = filename.replace(/\.[^/.]+$/, ""); + } + + toJSON(): SlideJSON { + return { + video: { + name: this._videoFileName, + format: this._videoFileFormat, + muted: this._muted, + }, + text: { + x: this._textX, + y: this._textY, + font: { + family: this._fontFamily, + bold: this._fontBold, + textToHeightRatio: this._fontTextToHeightRatio, + background: this._fontBackground || false, + }, + value: this._text, + }, + thumbnail: { + format: this.videoThumbnailFormat, + }, + }; + } + + // this function is only added to prevent any breaks in presentation mode + /** + * this function is only added to prevent any breaks in presentation mode + * and it will be deprecated and removed in future version in favour of LyricManager + * @param mode + * @param sepBy + * @returns + */ + splitText(mode = "words", sepBy: any = "6") { + let subtitledText = []; + + if (mode === "words") { + sepBy *= 1; + let textSplit = this._text.split(" "); + for (let i = 0; i < textSplit.length; i += sepBy) { + subtitledText.push(textSplit.slice(i, i + sepBy).join(" ")); + } + } else { + subtitledText = this._text.split(sepBy); + } + console.log(subtitledText); + return subtitledText; + } +} + +export default Slide; diff --git a/src/renderer/js/Classes/SlideClass.js b/src/renderer/js/Classes/SlideClass.js deleted file mode 100644 index 86fc054..0000000 --- a/src/renderer/js/Classes/SlideClass.js +++ /dev/null @@ -1,182 +0,0 @@ -import slide from '../../asset/resource/Presentation1.png'; - -/** Class representing a slide. - * @class - */ -class Slide { - #videoFileFormat; - #videoFileName; - - get textX() { - return this._textX; - } - - get textY() { - return this._textY; - } - - get text() { - return this._text; - } - - get videoFileName() { - return this.#videoFileName; - } - - get videoFileFormat() { - return this.#videoFileFormat; - } - - get loop() { - return this._loop; - } - - set text(text) { - this._text = text; - } - - /** - * - * @param {string} videoFileName - */ - set videoFileName(videoFileName) { - let nameSplitted = videoFileName.split(".") - this.#videoFileFormat = nameSplitted.pop(); - console.log(nameSplitted, this.#videoFileFormat) - this.#videoFileName = nameSplitted.join("."); - } - - - /** - * - * @param {Object} params - * @param {string} params.text - * @param {string} params.videoFileName - * @param {number} params.X - * @param {number} params.Y - * @param {boolean} params.loop - * @param {string} params.videoFileFormat - * @param {string} params.videoThumbnailFormat - * @param {boolean} params.isRTL - */ - constructor({ - text = "Please Enter the text", - videoFileName = undefined, - isRTL = true, - X = 0, - Y = 0, - loop = true, - videoThumbnailFormat = "png", - videoFileFormat = "mp4" - } = {}) { - - /** @type{string} Video file name */ - this.#videoFileName = videoFileName; - /** @type{string} Video file format */ - this.#videoFileFormat = videoFileFormat; - /** @type{string} Video thumbnail format */ - this.videoThumbnailFormat = videoThumbnailFormat - /** @type{string} text attached to the video */ - this._text = text; - /** @type{number} text X position */ - this._textX = X || 0; - /** @type{number} text Y position */ - this._textY = Y || 0; - /** @type{boolean} weather to loop the video */ - this._loop = loop || true - }; - - /** - * Split text according to the delimiters - * @param {string} mode - * @param {string} sepBy - * @return {string[]} - */ - splitText(mode = "words", sepBy = "6") { - let subtitledText = [] - - if (mode === "words") { - sepBy *= 1 - let textSplit = this._text.split(" ") - for (let i = 0; i < textSplit.length; i += sepBy) { - subtitledText.push(textSplit.slice(i, i + sepBy).join(" ")) - } - - } else { - subtitledText = this._text.split(sepBy) - } - console.log(subtitledText) - return subtitledText - } - - - /** - * Create video thumbnail - * @param {string} basePath - * @param {number} thumbAtPercent - * @return {Promise} base64 Image - */ - - createVideoCoverImage(basePath, thumbAtPercent = 0.2) { - return new Promise((resolve, reject) => { - // define a canvas to have the same dimension as the video - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = 1920; - canvas.height = 1080; - if (this.videoFileName !== undefined) { - // load the file to a video player - const videoPlayer = document.createElement('video'); - videoPlayer.setAttribute('src', "file://" + basePath + "/" + this.videoFileName + "." + this.videoFileFormat); - videoPlayer.load(); - videoPlayer.addEventListener('error', (ex) => { - reject("error when loading video file", ex); - }); - // load metadata of the video to get video duration and dimensions - videoPlayer.addEventListener('loadedmetadata', () => { - // seek to user defined timestamp (in seconds) if possible - if (videoPlayer.duration < thumbAtPercent) { - reject("video is too short."); - return; - } - // delay seeking or else 'seeked' event won't fire on Safari - setTimeout(() => { - videoPlayer.currentTime = videoPlayer.duration * thumbAtPercent; - }, 200); - // extract video thumbnail once seeking is complete - videoPlayer.addEventListener('seeked', () => { - console.log('video is now paused at %ss.', thumbAtPercent); - // draw the video frame to canvas - ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height); - resolve(canvas.toDataURL('image/' + this.videoThumbnailFormat, 0.8)); - }); - }); - } else { - ctx.fillStyle = "#fff" - ctx.fillRect(0, 0, canvas.width, canvas.height); - resolve(canvas.toDataURL('image/' + this.videoThumbnailFormat, 0.8)) - } - }); - } - - - /** - * Converts Slide - * @return {Object} - */ - toJSON() { - return { - text: this._text, - videoFileName: this.#videoFileName, - videoFileFormat: this.#videoFileFormat, - videoThumbnailFormat: this.videoThumbnailFormat, - x: this.textX, - y: this.textY, - loop: this.loop, - } - } - - -} - -export default Slide; \ No newline at end of file diff --git a/src/renderer/js/Classes/SlideManager.js b/src/renderer/js/Classes/SlideManager.js new file mode 100644 index 0000000..bdedc93 --- /dev/null +++ b/src/renderer/js/Classes/SlideManager.js @@ -0,0 +1,106 @@ +import Slide from "./Slide"; + +/** + * Manages a collection of slides, providing methods to add, remove, and navigate slides. + * + * @class + * @classdesc SlideManager handles the storage and manipulation of Slide objects, maintaining the current slide index and providing accessors for slides and indices. + * + * @param {Slide[]} [initialSlides=[]] - Optional array of initial slides to populate the manager. + * + * @example + * const manager = new SlideManager([slide1, slide2]); + * manager.addSlide(slide3); + * manager.removeSlide(); + * console.log(manager.currentSlide); + */ +class SlideManager { + _slides = []; + #current = -1; + _onSlideChange; + + constructor(props) { + console.log(props); + this._slides = [...props.slides]; + this._onSlideChange = props.onSlideChange; + this.#current = this._slides.length > 0 ? 0 : -1; + console.log(`${this.constructor.name} initialized `, this._onSlideChange); + } + + addSlide(slide, index = Number(this.#current) + 1) { + // Handle edge case when no slides exist + if (this._slides.length === 0) { + this._slides.push(slide); + this.#current = 0; + console.log("Added first slide, new length:", this._slides.length); + this.setCurrent(this.#current); + return this.#current; + } + + index = Math.min(index, this._slides.length); + console.log("Adjusted index:", index); + + this._slides.splice(index, 0, slide); + this.setCurrent(index); + + return this.#current; + } + + removeSlide() { + if (this.#current === -1) return; + this._slides.splice(this.#current, 1); + let removedSlideIdx = this.#current; + this.setCurrent(Math.max(0, this.#current - 1)); + return removedSlideIdx; + } + + updateTextFont(fontName, index = this.#current) { + this._slides[index].setFontName(fontName); + } + updateTextPosition(x, y, index = this.#current) { + this._slides[index].setTextPosition({ x, y }); + } + updateSlideText(text, index = this.#current) { + this._slides[index].setText(text); + } + updateSlideVideo(videoName, index = this.#current) { + this._slides[index].setVideoFileName(videoName); + } + + /** + * Toggles the muted state of the slide at the specified index. + * If no index is provided, toggles the muted state of the current slide. + * + * @param {number} [index=this.#current] - The index of the slide to toggle mute on. + * @returns {boolean} - The new muted state of the slide (true if muted, false otherwise). + */ + toggleMuteSlide(index = this.#current) { + return this._slides[index].toggleMuted(); + } + + toggleSlideBackground(index = this.#current) { + return this._slides[index].toggleBackground(); + } + + get currentSlide() { + return this._slides[this.#current]; + } + + get currentIndex() { + return this.#current; + } + + setCurrent(index) { + this.#current = index; + this._onSlideChange?.(); + } + + get allSlides() { + return [...this._slides]; + } + + toJSON() { + return this._slides; + } +} +export default SlideManager; diff --git a/src/renderer/js/Classes/TextEditorClass.js b/src/renderer/js/Classes/TextEditorClass.js new file mode 100644 index 0000000..8a465f1 --- /dev/null +++ b/src/renderer/js/Classes/TextEditorClass.js @@ -0,0 +1,57 @@ +export default class TextEditorArea { + constructor(props) { + this.textArea = props.textAreaElement; + this.fontSelector = props.fontSelectorElement; + this.backgroundBtn = props.backgroundBtnElemnt; + // this.boldBtn = props.boldBtn; + // this.fontSizeField = props.fontSizeElement; + this.onTextEdited = props.onTextEditedFn; + this.onFontSelected = props.onFontSelectedFn; + this.onBackgroundToggle = props.onBackgroundToggle; + slideFiles.allFonts().then((fonts) => { + // console.log(fonts); + this.initializeFontSelector(fonts); + }); + } + + createFontOption(fontName, face) { + let option = document.createElement("option"); + const regex = new RegExp(`\\bbold\\b`, "gi"); + fontName = fontName.replace(regex, ""); + option.text = `${fontName} ابجد هوز`; + option.value = fontName; + option.style = `font-family: ${fontName}; padding:2px; font-size:18pt; `; + return option; + } + + initializeFontSelector(fonts) { + fonts.forEach((font) => { + this.fontSelector.appendChild(this.createFontOption(font)); + }); + } + _attachEventListeners() { + console.log(this.textArea); + this.textArea?.addEventListener("input", (e) => { + this.onTextEdited?.(e.currentTarget.value); + }); + this.fontSelector?.addEventListener("change", (e) => { + const fontName = e.target.value; + e.target.style = `font-family: ${fontName}; font-size: 16pt;`; + this.onFontSelected?.(fontName); + }); + this.backgroundBtn?.addEventListener("input", (e) => { + console.log(e.target.value); + this.onBackgroundToggle?.(e.target.value); + }); + } + + setTextArea(text) { + this.textArea.value = text; + } + renderBackgroundBtn(state) { + this.backgroundBtn.checked = state; + } + setFontSelector(fontName) { + this.fontSelector.value = fontName; + } +} diff --git a/src/renderer/js/Classes/Utils.js b/src/renderer/js/Classes/Utils.js new file mode 100644 index 0000000..2f7772a --- /dev/null +++ b/src/renderer/js/Classes/Utils.js @@ -0,0 +1,108 @@ +class Utils { + constructor(parameters) {} + + /** + * Create video thumbnail + * @param {string} basePath + * @param {number} thumbAtPercent + * @return {Promise} base64 Image + */ + + static createVideoCoverImage(filePath, thumbAtPercent = 0.2) { + return new Promise((resolve, reject) => { + // define a canvas to have the same dimension as the video + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = 1920; + canvas.height = 1080; + + // load the file to a video player + const videoPlayer = document.createElement("video"); + videoPlayer.setAttribute("src", "file://" + filePath); + videoPlayer.load(); + videoPlayer.addEventListener("error", (ex) => { + reject("error when loading video file", ex); + }); + // load metadata of the video to get video duration and dimensions + videoPlayer.addEventListener("loadedmetadata", () => { + // seek to user defined timestamp (in seconds) if possible + if (videoPlayer.duration < thumbAtPercent) { + reject("video is too short."); + return; + } + // delay seeking or else 'seeked' event won't fire on Safari + setTimeout(() => { + videoPlayer.currentTime = videoPlayer.duration * thumbAtPercent; + }, 200); + // extract video thumbnail once seeking is complete + videoPlayer.addEventListener("seeked", () => { + console.log("video is now paused at %ss.", thumbAtPercent); + // draw the video frame to canvas + ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height); + resolve(canvas.toDataURL("image/" + this.videoThumbnailFormat, 0.8)); + }); + }); + }); + } + + // Helper functions for calculating bounding boxes + static getCorner(pivotX, pivotY, diffX, diffY, angle) { + const distance = Math.sqrt(diffX * diffX + diffY * diffY); + + // Find angle from pivot to corner + angle += Math.atan2(diffY, diffX); + + // Get new x and y coordinates + const x = pivotX + distance * Math.cos(angle); + const y = pivotY + distance * Math.sin(angle); + + return { x, y }; + } + + // Calculate client rect accounting for rotation + static getClientRect(rotatedBox) { + const { x, y, width, height } = rotatedBox; + const rad = rotatedBox.rotation; + + const p1 = this.getCorner(x, y, 0, 0, rad); + const p2 = this.getCorner(x, y, width, 0, rad); + const p3 = this.getCorner(x, y, width, height, rad); + const p4 = this.getCorner(x, y, 0, height, rad); + + const minX = Math.min(p1.x, p2.x, p3.x, p4.x); + const minY = Math.min(p1.y, p2.y, p3.y, p4.y); + const maxX = Math.max(p1.x, p2.x, p3.x, p4.x); + const maxY = Math.max(p1.y, p2.y, p3.y, p4.y); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + + // Calculate total bounding box of multiple shapes + static getTotalBox(boxes) { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + boxes.forEach((box) => { + minX = Math.min(minX, box.x); + minY = Math.min(minY, box.y); + maxX = Math.max(maxX, box.x + box.width); + maxY = Math.max(maxY, box.y + box.height); + }); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } +} + +module.exports = Utils; diff --git a/src/renderer/js/Classes/VideoToolbarClass.js b/src/renderer/js/Classes/VideoToolbarClass.js new file mode 100644 index 0000000..5dbfc13 --- /dev/null +++ b/src/renderer/js/Classes/VideoToolbarClass.js @@ -0,0 +1,28 @@ +class VideoToolbar { + #muteButton; + #replaceVideoButton; + constructor(props) { + this.container = props.container; + this.onMuteBtnClicked = props.onMuteButton; + this.onReplaceBtnClicked = props.onReplaceBtn; + this.#muteButton = this.container.querySelector("#muteVideoBtn"); + this.#replaceVideoButton = this.container.querySelector("#replaceVideoBtn"); + + console.log(`${this.constructor.name} initialized `); + } + + _attachEventListeners() { + this.#muteButton.addEventListener( + "click", + this.onMuteBtnClicked.bind(this) + ); + } + + changeMuteButtonIcon(muted) { + console.log(this.#muteButton); + this.#muteButton.querySelector(".material-symbols-outlined").innerText = + muted ? "volume_off" : "volume_up"; + } +} + +export default VideoToolbar; diff --git a/src/renderer/openFileDialog/index.html b/src/renderer/openFileDialog/index.html index 4180c84..231ab91 100644 --- a/src/renderer/openFileDialog/index.html +++ b/src/renderer/openFileDialog/index.html @@ -37,8 +37,11 @@
    -
    - +
    + + + +
    diff --git a/src/renderer/openFileDialog/index.js b/src/renderer/openFileDialog/index.js index 39e9bbb..c3a1c74 100644 --- a/src/renderer/openFileDialog/index.js +++ b/src/renderer/openFileDialog/index.js @@ -1,21 +1,31 @@ -import "bootstrap" -import "./index.scss" -const selectFileBtn = document.querySelector("#selectFileBtn") -const openFileBtn = document.querySelector("#openFileBtn") -const fileDestField = document.querySelector("#fileDestContainer") +import "bootstrap"; +import "./index.scss"; +const selectFileBtn = document.querySelector("#selectFileBtn"); +const openFileBtn = document.querySelector("#openFileBtn"); +const presentFileBtn = document.querySelector("#presentFileBtn"); +const fileDestField = document.querySelector("#fileDestContainer"); //input[name=mode]:checked selectFileBtn.addEventListener("click", () => { - file.open("o").then(([dist])=>{ - fileDestField.value = dist - }) -}) + file.open("o").then(([dist]) => { + fileDestField.value = dist; + }); +}); -openFileBtn.addEventListener("click",()=>{ - if (fileDestField.value!== ""){ - let mode = document.querySelector("input[name=mode]:checked").value - let sepBy = document.querySelector(`#${mode}`).value - let filePath = fileDestField.value - let params = {sepBy, mode, filePath} - file.fileOpened(params) - } -}) \ No newline at end of file +openFileBtn.addEventListener("click", () => { + if (fileDestField.value !== "") { + let mode = document.querySelector("input[name=mode]:checked").value; + let sepBy = document.querySelector(`#${mode}`).value; + let filePath = fileDestField.value; + let params = { sepBy, mode, filePath, present: false }; + file.fileOpened(params); + } +}); +presentFileBtn.addEventListener("click", () => { + if (fileDestField.value !== "") { + let mode = document.querySelector("input[name=mode]:checked").value; + let sepBy = document.querySelector(`#${mode}`).value; + let filePath = fileDestField.value; + let params = { sepBy, mode, filePath, present: true }; + file.fileOpened(params); + } +}); diff --git a/src/renderer/preload.js b/src/renderer/preload.js index 7aeb485..8d2c263 100644 --- a/src/renderer/preload.js +++ b/src/renderer/preload.js @@ -2,29 +2,38 @@ // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts // Preload (Isolated World) -const {contextBridge, ipcRenderer} = require('electron') +const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("file", { - open: (mode) => ipcRenderer.invoke("file-dialog-open", mode), - save: (fileContent) => ipcRenderer.invoke("file-save", fileContent), - saveAndQuit: (fileContent) => ipcRenderer.invoke("save-quit", fileContent), - copyVideo: (vidPath) => ipcRenderer.invoke("copy-video", vidPath), - fileOpened: (fileParams) => ipcRenderer.invoke("file-opened", JSON.parse(JSON.stringify(fileParams))), - onFileParams: (callback) => ipcRenderer.on("file-params", (_event, fileParams) => callback(fileParams)) -}) + open: (mode) => ipcRenderer.invoke("file-dialog-open", mode), + fileOpened: (fileParams) => + ipcRenderer.invoke("file-opened", JSON.parse(JSON.stringify(fileParams))), + onFileParams: (callback) => + ipcRenderer.on("file-params", (_event, fileParams) => callback(fileParams)), + save: (fileContent) => ipcRenderer.invoke("file-save", fileContent), + onSaveBeforeQuit: (callback) => ipcRenderer.on("save-before-quit", callback), + saveAndQuit: (fileContent) => ipcRenderer.invoke("save-quit", fileContent), + saveDone: () => ipcRenderer.send("save-done"), +}); -contextBridge.exposeInMainWorld("thumbs", { - create: (props) => ipcRenderer.invoke("create-thumb", props) -}) +/* contextBridge.exposeInMainWorld("thumbs", { + create: (props) => ipcRenderer.invoke("create-thumb", props), +}); + */ -contextBridge.exposeInMainWorld("comm", { - toPresentation: (props) => ipcRenderer.send("to-presentation", props), - onSlideshowInitialized: (callback) => ipcRenderer.on("slideshow:init", (_e) => callback()), - startSlideshow: (content) => ipcRenderer.invoke("slideshow:start", content), - onSlideshowDestroy: (callback) => ipcRenderer.on("slideshow:destroy", (_e) => callback()), - -}) +contextBridge.exposeInMainWorld("slideFiles", { + addSlideFiles: (props) => ipcRenderer.send("addSlideFiles", props), + allFonts: () => ipcRenderer.invoke("getSystemFonts"), +}); +contextBridge.exposeInMainWorld("comm", { + toPresentation: (props) => ipcRenderer.send("to-presentation", props), + onSlideshowInitialized: (callback) => + ipcRenderer.on("slideshow:init", (_e) => callback()), + startSlideshow: (content) => ipcRenderer.invoke("slideshow:start", content), + onSlideshowDestroy: (callback) => + ipcRenderer.on("slideshow:destroy", (_e) => callback()), +}); /* ipcRenderer.on("file-opened",(event, basePath, fileContent)=>{ diff --git a/src/renderer/presentationView/renderer.js b/src/renderer/presentationView/renderer.js index f5104f0..aab8e8b 100644 --- a/src/renderer/presentationView/renderer.js +++ b/src/renderer/presentationView/renderer.js @@ -1,5 +1,5 @@ import Show from "../js/Classes/ShowClass"; -import Slide from "../js/Classes/SlideClass"; +import Slide from "../js/Classes/Slide"; import hotkeys from 'hotkeys-js'; import "./index.scss"; diff --git a/src/renderer/presenterView/js/fileOpen.js b/src/renderer/presenterView/js/fileOpen.js index 960caf2..90a9c14 100644 --- a/src/renderer/presenterView/js/fileOpen.js +++ b/src/renderer/presenterView/js/fileOpen.js @@ -1,4 +1,4 @@ -import Slide from "../../js/Classes/SlideClass"; +import Slide from "../../js/Classes/Slide"; import ShowPresenterView from "../../js/Classes/ShowPresenterViewClass"; import hotkeys from "hotkeys-js"; diff --git a/src/utils/MediaResponderClass.js b/src/utils/MediaResponderClass.js new file mode 100644 index 0000000..5ac38bc --- /dev/null +++ b/src/utils/MediaResponderClass.js @@ -0,0 +1,104 @@ +import fs from "fs"; +import path, { parse as pathParse } from "path"; +import mime from "mime"; + +/** + * Generic media responder with range support (videos, images, audio, etc.) + */ +class MediaResponder { + constructor(request, currentProject) { + this.request = request; + this.currentProject = currentProject; + this.headers = new Headers(); + this.rangeText = request.headers.get("range"); + this.status = 200; + } + + async handle() { + const filePath = decodeURIComponent( + this.request.url.slice("media://".length) + ); + const parsed = pathParse(filePath); + const ext = parsed.ext; + const baseName = parsed.name + ext; + + const mimeType = mime.getType(ext) || "application/octet-stream"; + this.headers.set("Content-Type", mimeType); + + const localFile = + this.currentProject.notInArchive[baseName] || + path.join(this.currentProject.projectTempFolder, "videos", baseName); + if (typeof localFile === "string") { + return this.#respondFromFile(localFile); + } + + if (localFile instanceof Uint8Array) { + return this.#respondFromBuffer(localFile); + } + + const zipEntryPath = `videos\\${baseName}`; + const buf = await this.currentProject.fileStream(zipEntryPath); + return this.#respondFromBuffer(buf); + } + + #parseRange(size) { + if (!this.rangeText) return null; + const match = this.rangeText.match(/bytes=(\d+)-(\d*)/); + if (!match) return null; + const start = parseInt(match[1], 10); + const end = match[2] ? parseInt(match[2], 10) : size - 1; + if (start >= size || end >= size) return null; + return { + start, + end, + length: end - start + 1, + rangeHeader: `bytes ${start}-${end}/${size}`, + }; + } + + #respondFromFile(filePath) { + const stat = fs.statSync(filePath); + const totalSize = stat.size; + const range = this.#parseRange(totalSize); + + if (range) { + this.headers.set("Accept-Ranges", "bytes"); + this.headers.set("Content-Length", `${range.length}`); + this.headers.set("Content-Range", range.rangeHeader); + this.status = 206; + const stream = fs.createReadStream(filePath, { + start: range.start, + end: range.end, + }); + return new Response(stream, { + headers: this.headers, + status: this.status, + }); + } + + this.headers.set("Content-Length", `${totalSize}`); + const stream = fs.createReadStream(filePath); + return new Response(stream, { headers: this.headers, status: this.status }); + } + + #respondFromBuffer(buffer) { + const totalSize = buffer.length; + const range = this.#parseRange(totalSize); + + if (range) { + this.headers.set("Accept-Ranges", "bytes"); + this.headers.set("Content-Length", `${range.length}`); + this.headers.set("Content-Range", range.rangeHeader); + this.status = 206; + return new Response(buffer.subarray(range.start, range.end), { + headers: this.headers, + status: this.status, + }); + } + + this.headers.set("Content-Length", `${totalSize}`); + return new Response(buffer, { headers: this.headers, status: this.status }); + } +} + +export default MediaResponder; diff --git a/src/workingFile.ts b/src/workingFile.ts new file mode 100644 index 0000000..6c5fd8e --- /dev/null +++ b/src/workingFile.ts @@ -0,0 +1,317 @@ +import { tmpdir } from "os"; +import * as path from "path"; +import * as fs from "fs"; +import * as archiver from "archiver"; +import * as tar from "tar-stream"; +import { buffer } from "stream/consumers"; +import { Stream } from "stream"; + +enum ProjectOpenMode { + NEW, + EDIT, + PRESENT, +} +interface addVideoSlideFileInterface { + imgBuffer: Buffer; + imgFileName: string; + videoFilePath: string; + videoFileName: string; +} +class WorkingFile { + /** + * shows the files that are added to the archive bit not available in the stream reader + * @type {Record} + */ + #notInArchive: Record = {}; + + #addedToArchive: string[] = []; + + /** + * @type {string} + */ + #sepMode; + + /** + * separation delimiter + * @type{number || string} + */ + #delimiter; + + /** + * Saved data content of the file JSON slideshow + */ + #lastSavedData: string; + + /** + * shows weather the file is opened or not + * @type{boolean} + */ + #isEditingOpened = false; + + /** + * Zip Object file + * + */ + #fileCreator: archiver.Archiver; + #writeStream: fs.WriteStream; + + #projectMode; + + /** + * the show file path + * @type {string} + */ + #filePath; + needsCreator: boolean; + get notInArchive() { + return this.#notInArchive; + } + + get isOpened() { + return this.#isEditingOpened; + } + + get projectPath() { + return this.#filePath; + } + + get basePath() { + const tmpAppPath = path.join(tmpdir(), "choirSlides"); + return tmpAppPath; + } + + /** + * File path parsed + * @type {ParsedPath} + */ + get #projectFilePathParsed() { + return path.parse(this.#filePath); + } + + get projectTempFolder() { + const projectTempPath = path.join(this.basePath, this.projectName); + if (!fs.existsSync(projectTempPath)) { + fs.mkdirSync(projectTempPath, { recursive: true }); + } + return projectTempPath; + } + + /** + * Opened file name + * @type {string} + */ + get projectName() { + return this.#projectFilePathParsed.name; + } + + /** + * Adds image and video files to the archive and tracks the video file path. + * + */ + + addVideoSlideFiles(props: addVideoSlideFileInterface) { + const { imgBuffer, imgFileName, videoFilePath, videoFileName } = props; + this.#fileCreator.append(imgBuffer, { name: `videos/${imgFileName}.png` }); + this.#fileCreator.file(videoFilePath, { name: `videos/${videoFileName}` }); + console.log("added Video Files"); + + this.#notInArchive[videoFileName.toLowerCase()] = videoFilePath; + this.#notInArchive[`${imgFileName}.png`.toLowerCase()] = imgBuffer; + } + + #extractProjectFile( + tarFilePath: string = this.#filePath, + outputDir: string = this.projectTempFolder + ) { + return new Promise((resolve, reject) => { + const extract = tar.extract(); + + extract.on("entry", (header, stream, next) => { + const outputPath = path.join(outputDir, header.name); + + // Ensure directories exist + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + const writeStream = fs.createWriteStream(outputPath); + stream.pipe(writeStream); + + writeStream.on("finish", next); // Wait until write completes + writeStream.on("error", reject); + stream.on("error", reject); + }); + + extract.on("finish", resolve); + extract.on("error", reject); + + fs.createReadStream(tarFilePath).pipe(extract); + }); + } + + constructor(data: any) { + this.#filePath = data.filePath || ""; + this.#sepMode = data.mode; + this.#delimiter = data.sepBy; + + if (data.present) { + this.#projectMode = ProjectOpenMode.PRESENT; + } else if (fs.existsSync(this.#filePath)) { + this.#projectMode = ProjectOpenMode.EDIT; + } else { + this.#projectMode = ProjectOpenMode.NEW; + } + + // Initialize based on mode + this.needsCreator = this.#projectMode !== ProjectOpenMode.PRESENT; + } + + async editProject() { + if (this.#projectMode === ProjectOpenMode.NEW) { + this.#lastSavedData = "[]"; + } else { + await this.#extractProjectFile(); + this.#lastSavedData = fs.readFileSync( + path.join(this.projectTempFolder, "slides.json"), + "utf-8" + ); + console.log( + path.join(this.projectTempFolder, "slides.json"), + this.#lastSavedData + ); + } + this.#isEditingOpened = true; + this.#fileCreator = archiver("tar", { + gzip: false, // Set to true if you want .tar.gz + }); + this.#writeStream = fs.createWriteStream(this.#filePath); + this.#fileCreator.pipe(this.#writeStream); + + this.#writeStream?.on("drain", () => { + console.log("Drained adding a file to zip"); + }); + this.#writeStream?.on("warning", (e: any) => { + console.log(`Warning while adding a file to zip: ${e.message}`); + }); + this.#writeStream?.on("finish", () => { + console.log("Finish adding a file to zip"); + }); + this.#writeStream?.on("close", () => { + console.log("closing zip"); + }); + this.#writeStream?.on("data", (data: any) => { + console.log("on data"); + }); + this.#writeStream?.on("entry", (data: any) => { + console.log("on entry"); + }); + } + + async saveProject(content: string) { + console.log("Saving"); + this.#addExtractedFilesToZip(); + console.log("writing"); + fs.writeFileSync(path.join(this.projectTempFolder, "slides.json"), content); + console.log("slides"); + this.#fileCreator.append(Buffer.from(content, "utf8"), { + name: "slides.json", + date: new Date(2025, 7, 8, 23, 4), + }); + + console.log(this.#fileCreator); + } + + async presentProject() { + const slidesStream: any = await this.fileStream("slides.json"); + this.#lastSavedData = (await buffer(slidesStream)).toString("utf8"); + } + + closeProject(slidesContent: string) { + console.log("Close Called"); + return new Promise((res, rej) => { + console.log("Created Promise"); + if (this.#isEditingOpened) { + console.log("Saving"); + this.#addExtractedFilesToZip(); + const slidesPath = path.join(this.projectTempFolder, "slides.json"); + fs.writeFileSync(slidesPath, slidesContent); + + this.#writeStream.on("finish", () => { + console.log(this.#fileCreator.pointer() + " total bytes"); + console.log( + "archiver has been finalized and the output file descriptor has closed." + ); + res(true); + }); + this.#writeStream.on("error", () => { + console.error("Error writing ZIP file:"); + rej("Error in Piping"); + }); + this.#writeStream.on("end", () => { + console.log("Data has been drained"); + }); + + this.#fileCreator.pipe(this.#writeStream); + console.log("piping"); + + console.log("finalizing"); + this.#fileCreator.finalize(); + } + }); + } + #addExtractedFilesToZip() { + const videosPath = path.join(this.projectTempFolder, "videos"); + const filesInPath = new Set( + fs.existsSync(videosPath) ? fs.readdirSync(videosPath) : [] + ); + const filesInArray = new Set(this.#addedToArchive); + const filesToBeAdded = [...filesInPath].filter( + (element: string) => !filesInArray.has(element) + ); + if (filesToBeAdded.length > 0) { + for (const f of filesToBeAdded) { + const fp = path.join(videosPath, f); + console.log({ fp, f }); + this.#fileCreator.append(fs.createReadStream(fp), { + name: `videos/${f}`, + }); + this.#addedToArchive.push(`${f}`); + } + } + } + + toObject() { + return { + filePath: this.#filePath, + sepBy: this.#delimiter, + mode: this.#sepMode, + content: this.#lastSavedData, + }; + } + + async fileStream(zipfilePath: string) { + return new Promise((resolve, reject) => { + const extract = tar.extract(); + const tarStream = fs.createReadStream(this.#filePath); + + let found = false; + + extract.on("entry", (header, stream, next) => { + if (header.name === zipfilePath) { + found = true; + resolve(stream); // Pass the file stream out + // Don't call `next()` here — let consumer drain the stream + } else { + stream.resume(); // Skip this entry + next(); + } + }); + + extract.on("finish", () => { + if (!found) reject(new Error(`File not found: ${zipfilePath}`)); + }); + + tarStream.pipe(extract); + }); + } +} + +export default WorkingFile; diff --git a/src/workingFile.js b/src/workingFileTemp.js similarity index 95% rename from src/workingFile.js rename to src/workingFileTemp.js index 024516b..faf5491 100644 --- a/src/workingFile.js +++ b/src/workingFileTemp.js @@ -69,7 +69,7 @@ class WorkingFile { } get #workingTempDir() { - return "temp-" + this.#projectRandom + return "videos" } /** @@ -124,13 +124,13 @@ class WorkingFile { } openProject() { - let zip = new AdmZip(this.#filePath) + /*let zip = new AdmZip(this.#filePath) zip.extractAllTo(this.#openedFileDirectory) fs.renameSync(path.join(this.#openedFileDirectory, "videos"), this.basePath) fs.renameSync(path.join(this.#openedFileDirectory, "slides.json"), this.#baseFilePath) fswin.setAttributesSync(this.basePath, {IS_HIDDEN: true}); - fswin.setAttributesSync(this.#baseFilePath, {IS_HIDDEN: true}); - this.#lastSavedData = fs.readFileSync(path.join(this.#openedFileDirectory, this.#projectRandom + ".json"), {encoding: "utf8"}) + fswin.setAttributesSync(this.#baseFilePath, {IS_HIDDEN: true});*/ + this.#lastSavedData = fs.readFileSync(this.#filePath, {encoding: "utf8"}) this.#isOpened = true console.log(`Opening ${this.#projectRandom}`) } @@ -161,6 +161,8 @@ class WorkingFile { } closeProject() { + this.#isOpened = false + return; if (this.#isOpened) { console.log(`Closing ${this.#projectRandom}`) fsp.rm(this.#baseFilePath).then(() => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..72cfccc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "es6", + "target": "es2024", + "jsx": "react", // If you are using React + "allowJs": true, + "moduleResolution": "node" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" // If you are using React +, "src/renderer/js/Classes/SlideManager.js" ] +} \ No newline at end of file diff --git a/webpack.main.config.js b/webpack.main.config.js index 276bcba..d685532 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -1,11 +1,15 @@ module.exports = { - /** - * This is the main entry point for your application, it's the first file - * that runs in the main process. - */ - entry: './src/index.js', - // Put your normal webpack config below here - module: { - rules: require('./webpack.rules'), - }, - }; \ No newline at end of file + /** + * This is the main entry point for your application, it's the first file + * that runs in the main process. + */ + entry: "./src/index.js", + + // Put your normal webpack config below here + module: { + rules: require("./webpack.rules"), + }, + resolve: { + extensions: [".ts", ".js"], + }, +}; diff --git a/webpack.renderer.config.js b/webpack.renderer.config.js index c363476..3a2d130 100644 --- a/webpack.renderer.config.js +++ b/webpack.renderer.config.js @@ -1,24 +1,22 @@ -const rules = require('./webpack.rules'); +const rules = require("./webpack.rules"); rules.push({ test: /\.scss$/, use: [ - { loader: 'style-loader' }, - { loader: 'css-loader' }, + { loader: "style-loader" }, + { loader: "css-loader" }, { - loader: 'sass-loader' + loader: "sass-loader", }, { - loader: 'postcss-loader', + loader: "postcss-loader", options: { postcssOptions: { plugins: function () { - return [ - require('autoprefixer') - ]; - } - } - } + return [require("autoprefixer")]; + }, + }, + }, }, ], }); @@ -29,7 +27,10 @@ module.exports = { rules, }, output: { - publicPath: './../', - assetModuleFilename:'[name][ext]' + publicPath: "./../", + assetModuleFilename: "[name][ext]", }, -}; \ No newline at end of file + resolve: { + extensions: [".ts", ".js"], + }, +}; diff --git a/webpack.rules.js b/webpack.rules.js index f6e0545..6f31aad 100644 --- a/webpack.rules.js +++ b/webpack.rules.js @@ -1,44 +1,38 @@ module.exports = [ - // Add support for native node modules - { - // We're specifying native_modules in the test because the asset relocator loader generates a - // "fake" .node file which is really a cjs file. - test: /native_modules[/\\].+\.node$/, - use: 'node-loader', + // Add support for native node modules + { + // We're specifying native_modules in the test because the asset relocator loader generates a + // "fake" .node file which is really a cjs file. + test: /native_modules[/\\].+\.node$/, + use: "node-loader", + }, + { + // Note: I dont have `svg` here because I run my .svg through the `@svgr/webpack` loader, + // but you can add it if you have no special requirements + test: /\.(gif|icns|ico|jpg|png|otf|eot|woff|woff2|ttf|svg)$/, + type: "asset/resource", + generator: { + outputPath: "./", }, - { - // Note: I dont have `svg` here because I run my .svg through the `@svgr/webpack` loader, - // but you can add it if you have no special requirements - test: /\.(gif|icns|ico|jpg|png|otf|eot|woff|woff2|ttf|svg)$/, - type: 'asset/resource', - generator:{ - outputPath:"./" - } - }, - { - test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, - parser: { amd: false }, - use: { - loader: '@vercel/webpack-asset-relocator-loader', - options: { - outputAssetBase: 'native_modules', - }, + }, + { + test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, + parser: { amd: false }, + use: { + loader: "@vercel/webpack-asset-relocator-loader", + options: { + outputAssetBase: "native_modules", }, }, - // Put your webpack loader rules in this array. This is where you would put - // your ts-loader configuration for instance: - /** - * Typescript Example: - * - * { - * test: /\.tsx?$/, - * exclude: /(node_modules|.webpack)/, - * loaders: [{ - * loader: 'ts-loader', - * options: { - * transpileOnly: true - * } - * }] - * } - */ - ]; \ No newline at end of file + }, + // Put your webpack loader rules in this array. This is where you would put + // your ts-loader configuration for instance: + /* * + * Typescript Example: + * */ + { + test: /\.ts$/, + include: /src/, + use: "ts-loader", + }, +];