Repo created
This commit is contained in:
parent
f3a6b3a320
commit
f954c78789
614 changed files with 135712 additions and 2 deletions
190
_scripts/ProcessLocalesPlugin.js
Normal file
190
_scripts/ProcessLocalesPlugin.js
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
const { existsSync, readFileSync } = require('fs')
|
||||
const { readFile } = require('fs/promises')
|
||||
const { join } = require('path')
|
||||
const { brotliCompress, constants } = require('zlib')
|
||||
const { promisify } = require('util')
|
||||
const { load: loadYaml } = require('js-yaml')
|
||||
|
||||
const brotliCompressAsync = promisify(brotliCompress)
|
||||
|
||||
const PLUGIN_NAME = 'ProcessLocalesPlugin'
|
||||
|
||||
class ProcessLocalesPlugin {
|
||||
constructor(options = {}) {
|
||||
this.compress = !!options.compress
|
||||
this.hotReload = !!options.hotReload
|
||||
|
||||
if (typeof options.inputDir !== 'string') {
|
||||
throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.')
|
||||
} else if (!existsSync(options.inputDir)) {
|
||||
throw new Error('ProcessLocalesPlugin: the specified input directory does not exist.')
|
||||
}
|
||||
this.inputDir = options.inputDir
|
||||
|
||||
if (typeof options.outputDir !== 'string') {
|
||||
throw new Error('ProcessLocalesPlugin: no output directory `outputDir` specified.')
|
||||
}
|
||||
this.outputDir = options.outputDir
|
||||
|
||||
/** @type {Map<string, any>} */
|
||||
this.locales = new Map()
|
||||
this.localeNames = []
|
||||
|
||||
/** @type {Map<string, any>} */
|
||||
this.cache = new Map()
|
||||
|
||||
this.filePaths = []
|
||||
this.previousTimestamps = new Map()
|
||||
this.startTime = Date.now()
|
||||
|
||||
/** @type {(updatedLocales: [string, string][]) => void|null} */
|
||||
this.notifyLocaleChange = null
|
||||
|
||||
this.loadLocales()
|
||||
}
|
||||
|
||||
/** @param {import('webpack').Compiler} compiler */
|
||||
apply(compiler) {
|
||||
const { CachedSource, RawSource } = compiler.webpack.sources
|
||||
const { Compilation, DefinePlugin } = compiler.webpack
|
||||
|
||||
new DefinePlugin({
|
||||
'process.env.HOT_RELOAD_LOCALES': this.hotReload
|
||||
}).apply(compiler)
|
||||
|
||||
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
||||
const IS_DEV_SERVER = !!compiler.watching
|
||||
|
||||
compilation.hooks.processAssets.tapPromise({
|
||||
name: PLUGIN_NAME,
|
||||
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
|
||||
}, async (_assets) => {
|
||||
// While running in the webpack dev server, this hook gets called for every incremental build.
|
||||
// For incremental builds we can return the already processed versions, which saves time
|
||||
// and makes webpack treat them as cached
|
||||
const promises = []
|
||||
|
||||
/** @type {[string, string][]} */
|
||||
const updatedLocales = []
|
||||
if (this.hotReload && !this.notifyLocaleChange) {
|
||||
console.warn('ProcessLocalesPlugin: Unable to live reload locales as `notifyLocaleChange` is not set.')
|
||||
}
|
||||
|
||||
for (let [locale, data] of this.locales) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
promises.push(new Promise(async (resolve) => {
|
||||
if (IS_DEV_SERVER && compiler.fileTimestamps) {
|
||||
const filePath = join(this.inputDir, `${locale}.yaml`)
|
||||
|
||||
const timestamp = compiler.fileTimestamps.get(filePath)?.safeTime
|
||||
|
||||
if (timestamp && timestamp > (this.previousTimestamps.get(locale) ?? this.startTime)) {
|
||||
this.previousTimestamps.set(locale, timestamp)
|
||||
|
||||
const contents = await readFile(filePath, 'utf-8')
|
||||
data = loadYaml(contents)
|
||||
} else {
|
||||
const { filename, source } = this.cache.get(locale)
|
||||
compilation.emitAsset(filename, source, { minimized: true })
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.removeEmptyValues(data)
|
||||
|
||||
let filename = `${this.outputDir}/${locale}.json`
|
||||
let output = JSON.stringify(data)
|
||||
|
||||
if (this.hotReload && compiler.fileTimestamps) {
|
||||
updatedLocales.push([locale, output])
|
||||
}
|
||||
|
||||
if (this.compress) {
|
||||
filename += '.br'
|
||||
output = await this.compressLocale(output)
|
||||
}
|
||||
|
||||
let source = new RawSource(output)
|
||||
|
||||
if (IS_DEV_SERVER) {
|
||||
source = new CachedSource(source)
|
||||
this.cache.set(locale, { filename, source })
|
||||
|
||||
// we don't need the unmodified sources anymore, as we use the cache `this.cache`
|
||||
// so we can clear this to free some memory
|
||||
this.locales.set(locale, null)
|
||||
}
|
||||
|
||||
compilation.emitAsset(filename, source, { minimized: true })
|
||||
|
||||
resolve()
|
||||
}))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
if (this.hotReload && this.notifyLocaleChange && updatedLocales.length > 0) {
|
||||
this.notifyLocaleChange(updatedLocales)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!compiler.watching) {
|
||||
// watch locale files for changes
|
||||
compilation.fileDependencies.addAll(this.filePaths)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadLocales() {
|
||||
const activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`))
|
||||
|
||||
for (const locale of activeLocales) {
|
||||
const filePath = join(this.inputDir, `${locale}.yaml`)
|
||||
|
||||
this.filePaths.push(filePath)
|
||||
|
||||
const contents = readFileSync(filePath, 'utf-8')
|
||||
const data = loadYaml(contents)
|
||||
this.locales.set(locale, data)
|
||||
|
||||
this.localeNames.push(data['Locale Name'] ?? locale)
|
||||
}
|
||||
}
|
||||
|
||||
async compressLocale(data) {
|
||||
const buffer = Buffer.from(data, 'utf-8')
|
||||
|
||||
return await brotliCompressAsync(buffer, {
|
||||
params: {
|
||||
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
[constants.BROTLI_PARAM_SIZE_HINT]: buffer.byteLength
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* vue-i18n doesn't fallback if the translation is an empty string
|
||||
* so we want to get rid of them and also remove the empty objects that can get left behind
|
||||
* if we've removed all the keys and values in them
|
||||
* @param {object|string} data
|
||||
*/
|
||||
removeEmptyValues(data) {
|
||||
for (const key of Object.keys(data)) {
|
||||
const value = data[key]
|
||||
if (typeof value === 'object') {
|
||||
this.removeEmptyValues(value)
|
||||
}
|
||||
|
||||
if (!value || (typeof value === 'object' && Object.keys(value).length === 0)) {
|
||||
delete data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProcessLocalesPlugin
|
||||
0
_scripts/_empty.js
Normal file
0
_scripts/_empty.js
Normal file
31
_scripts/_localforage.js
Normal file
31
_scripts/_localforage.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
import { readFile, writeFile } from '../src/renderer/helpers/android'
|
||||
|
||||
export function createInstance(_kwargs) {
|
||||
return {
|
||||
async getItem(key) {
|
||||
const dataLocationFile = await readFile('data://', 'data-location.json')
|
||||
if (dataLocationFile !== '') {
|
||||
const locationInfo = JSON.parse(dataLocationFile)
|
||||
const locationMap = Object.fromEntries(locationInfo.files.map((file) => { return [file.fileName, file.uri] }))
|
||||
if (key in locationMap) {
|
||||
return await readFile(locationMap[key])
|
||||
}
|
||||
}
|
||||
const data = await readFile('data://', key)
|
||||
return data
|
||||
},
|
||||
async setItem(key, value) {
|
||||
const dataLocationFile = await readFile('data://', 'data-location.json')
|
||||
if (dataLocationFile !== '') {
|
||||
const locationInfo = JSON.parse(dataLocationFile)
|
||||
const locationMap = Object.fromEntries(locationInfo.files.map((file) => { return [file.fileName, file.uri] }))
|
||||
if (key in locationMap) {
|
||||
await writeFile(locationMap[key], value)
|
||||
return
|
||||
}
|
||||
}
|
||||
await writeFile('data://', key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
124
_scripts/_setAppSplashTheme.mjs
Normal file
124
_scripts/_setAppSplashTheme.mjs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
|
||||
import { readFile, readdir, writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
|
||||
// sets the splashscreen & icon to one of three predefined themes (this makes it easier to tell, at a glance, which one is open)
|
||||
// - release (the default production look)
|
||||
// - nightly
|
||||
// OR
|
||||
// - development
|
||||
|
||||
const COLOURS = {
|
||||
RELEASE: {
|
||||
primary: '#f04242',
|
||||
secondary: '#14a4df',
|
||||
back: '#E4E4E4',
|
||||
backDark: '#212121'
|
||||
},
|
||||
// catppucin mocha theme colours
|
||||
NIGHTLY: {
|
||||
primary: '#cdd6f4',
|
||||
secondary: '#cdd6f4',
|
||||
back: '#1e1e2e',
|
||||
backDark: '#1e1e2e'
|
||||
},
|
||||
// inverted release colours
|
||||
DEVELOPMENT: {
|
||||
primary: '#E4E4E4',
|
||||
secondary: '#E4E4E4',
|
||||
back: '#f04242',
|
||||
backDark: '#f04242'
|
||||
},
|
||||
// blue from the logo as the background colour
|
||||
RC: {
|
||||
primary: '#E4E4E4',
|
||||
secondary: '#E4E4E4',
|
||||
back: '#14a4df',
|
||||
backDark: '#14a4df'
|
||||
},
|
||||
// solarised dark
|
||||
FEATURE_BRANCH: {
|
||||
primary: '#E4E4E4',
|
||||
secondary: '#E4E4E4',
|
||||
back: '#204b56',
|
||||
backDark: '#204b56'
|
||||
}
|
||||
}
|
||||
let colour = 'RELEASE'
|
||||
for (const key in COLOURS) {
|
||||
if (process.argv.indexOf(`--${key.toLowerCase().replaceAll('_', '-')}`) !== -1) {
|
||||
colour = key
|
||||
}
|
||||
}
|
||||
|
||||
const currentTheme = COLOURS[colour]
|
||||
|
||||
const scriptDir = fileURLToPath(import.meta.url)
|
||||
const drawablePath = join(scriptDir, '../../android/app/src/main/res/drawable/')
|
||||
|
||||
const foreground = join(drawablePath, 'ic_launcher_foreground.xml')
|
||||
let foregroundXML = (await readFile(foreground)).toString()
|
||||
foregroundXML = foregroundXML.replace(/<path android:fillColor="[^"]*?" android:strokeWidth="0\.784519" android:pathData="M 27/g, `<path android:fillColor="${currentTheme.primary}" android:strokeWidth="0.784519" android:pathData="M 27`)
|
||||
foregroundXML = foregroundXML.replace(/<path android:fillColor="[^"]*?" android:strokeWidth="0\.784519" android:pathData="M 18/g, `<path android:fillColor="${currentTheme.primary}" android:strokeWidth="0.784519" android:pathData="M 18`)
|
||||
foregroundXML = foregroundXML.replace(/<path android:fillColor="[^"]*?" android:strokeWidth="0\.784519" android:pathData="M 28/g, `<path android:fillColor="${currentTheme.secondary}" android:strokeWidth="0.784519" android:pathData="M 28`)
|
||||
await writeFile(foreground, foregroundXML)
|
||||
|
||||
const background = join(drawablePath, 'ic_launcher_background.xml')
|
||||
let backgroundXML = (await readFile(background)).toString()
|
||||
backgroundXML = backgroundXML.replace(/android:fillColor="[^"]*?" \/>/g, `android:fillColor="${currentTheme.back}" />`)
|
||||
await writeFile(background, backgroundXML)
|
||||
|
||||
/**
|
||||
* @warning name is passed into regex unsantised; should never be given user input
|
||||
* @param {string} xml
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
*/
|
||||
function replaceItem(xml, name, value) {
|
||||
return xml.replace(new RegExp(`<item name="android:${name}">[^"]*?<\/item>`), `<item name="android:${name}">${value}</item>`)
|
||||
}
|
||||
|
||||
async function constructThemePath(isDark = false, version = 0) {
|
||||
const resDirectory = join(scriptDir, '..', '..', 'android/app/src/main/res/')
|
||||
const files = await readdir(resDirectory)
|
||||
const versionsListed = files
|
||||
.filter(file => file.startsWith(`values${isDark ? '-night-' : '-'}v`))
|
||||
.map(file => parseInt(file.split('-v')[1]))
|
||||
if (versionsListed.indexOf(version) !== -1) {
|
||||
return join(resDirectory, `values${isDark ? '-night-' : '-'}v${version}`, 'themes.xml')
|
||||
} else {
|
||||
return join(resDirectory, 'values', 'themes.xml')
|
||||
}
|
||||
}
|
||||
|
||||
async function setValuesForThemeFile(values, isDark = false, version = 0) {
|
||||
const themePath = await constructThemePath(isDark, version)
|
||||
let themeXml = (await readFile(themePath)).toString()
|
||||
for (const key in values) {
|
||||
themeXml = replaceItem(themeXml, key, values[key])
|
||||
}
|
||||
await writeFile(themePath, themeXml)
|
||||
}
|
||||
|
||||
|
||||
await setValuesForThemeFile({
|
||||
windowSplashScreenBackground: currentTheme.back,
|
||||
windowSplashScreenIconBackgroundColor: currentTheme.back
|
||||
}, false, 31)
|
||||
|
||||
await setValuesForThemeFile({
|
||||
windowSplashScreenBackground: currentTheme.backDark,
|
||||
windowSplashScreenIconBackgroundColor: currentTheme.backDark
|
||||
}, true, 31)
|
||||
|
||||
await setValuesForThemeFile({
|
||||
windowSplashScreenBackground: currentTheme.back,
|
||||
windowSplashScreenIconBackgroundColor: currentTheme.back
|
||||
}, false, 33)
|
||||
|
||||
await setValuesForThemeFile({
|
||||
windowSplashScreenBackground: currentTheme.backDark,
|
||||
windowSplashScreenIconBackgroundColor: currentTheme.backDark
|
||||
}, true, 33)
|
||||
14
_scripts/add-iv-supported-links.js
Normal file
14
_scripts/add-iv-supported-links.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
const { readFile, writeFile } = require('fs/promises')
|
||||
const { join } = require('path')
|
||||
|
||||
;(async () => {
|
||||
const manifest = (await readFile(join(__dirname, '../android/app/src/main/AndroidManifest.xml'))).toString()
|
||||
const invidiousInstances = JSON.parse((await readFile(join(__dirname, '../static/invidious-instances.json'))).toString())
|
||||
const supportedLinks = manifest.match(/<!-- supported links -->[\s\S]*?<!-- \/supported links -->/gm)
|
||||
const instancesXml = invidiousInstances.map(({ url, cors}) => {
|
||||
return `<data android:host="${url.replace('https://', '')}" />`
|
||||
}).join('\n ')
|
||||
const postManifest = manifest.replace(supportedLinks[0], `<!-- supported links -->\n ${instancesXml}\n <!-- \/supported links -->`)
|
||||
await writeFile(join(__dirname, '../android/app/src/main/AndroidManifest.xml'), postManifest)
|
||||
})()
|
||||
53
_scripts/build.js
Normal file
53
_scripts/build.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const os = require('os')
|
||||
const builder = require('electron-builder')
|
||||
const config = require('./ebuilder.config.js')
|
||||
|
||||
const Platform = builder.Platform
|
||||
const Arch = builder.Arch
|
||||
const args = process.argv
|
||||
|
||||
let targets
|
||||
const platform = os.platform()
|
||||
|
||||
if (platform === 'darwin') {
|
||||
let arch = Arch.x64
|
||||
|
||||
if (args[2] === 'arm64') {
|
||||
arch = Arch.arm64
|
||||
}
|
||||
|
||||
targets = Platform.MAC.createTarget(['DMG', 'zip', '7z'], arch)
|
||||
} else if (platform === 'win32') {
|
||||
let arch = Arch.x64
|
||||
|
||||
if (args[2] === 'arm64') {
|
||||
arch = Arch.arm64
|
||||
}
|
||||
|
||||
targets = Platform.WINDOWS.createTarget(['nsis', 'zip', '7z', 'portable'], arch)
|
||||
} else if (platform === 'linux') {
|
||||
let arch = Arch.x64
|
||||
|
||||
if (args[2] === 'arm64') {
|
||||
arch = Arch.arm64
|
||||
}
|
||||
|
||||
if (args[2] === 'arm32') {
|
||||
arch = Arch.armv7l
|
||||
}
|
||||
|
||||
targets = Platform.LINUX.createTarget(['deb', 'zip', '7z', 'rpm', 'AppImage', 'pacman'], arch)
|
||||
}
|
||||
|
||||
builder
|
||||
.build({
|
||||
targets,
|
||||
config,
|
||||
publish: 'never'
|
||||
})
|
||||
.then(m => {
|
||||
console.log(m)
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
10
_scripts/clean.mjs
Normal file
10
_scripts/clean.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { rm } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
const BUILD_PATH = join(import.meta.dirname, '..', 'build')
|
||||
const DIST_PATH = join(import.meta.dirname, '..', 'dist')
|
||||
|
||||
await Promise.all([
|
||||
rm(BUILD_PATH, { recursive: true, force: true }),
|
||||
rm(DIST_PATH, { recursive: true, force: true })
|
||||
])
|
||||
215
_scripts/dev-runner.js
Normal file
215
_scripts/dev-runner.js
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
process.env.NODE_ENV = 'development'
|
||||
|
||||
const electron = require('electron')
|
||||
const webpack = require('webpack')
|
||||
const WebpackDevServer = require('webpack-dev-server')
|
||||
const kill = require('tree-kill')
|
||||
|
||||
const path = require('path')
|
||||
const { spawn } = require('child_process')
|
||||
|
||||
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
||||
|
||||
let electronProcess = null
|
||||
let manualRestart = null
|
||||
|
||||
const remoteDebugging = process.argv.indexOf('--remote-debug') !== -1
|
||||
const web = process.argv.indexOf('--web') !== -1
|
||||
|
||||
let mainConfig
|
||||
let rendererConfig
|
||||
let botGuardScriptConfig
|
||||
let webConfig
|
||||
let SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
|
||||
if (!web) {
|
||||
mainConfig = require('./webpack.main.config')
|
||||
rendererConfig = require('./webpack.renderer.config')
|
||||
botGuardScriptConfig = require('./webpack.botGuardScript.config')
|
||||
|
||||
SHAKA_LOCALES_TO_BE_BUNDLED = rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
delete rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
} else {
|
||||
webConfig = require('./webpack.web.config')
|
||||
}
|
||||
|
||||
if (remoteDebugging) {
|
||||
// disable dvtools open in electron
|
||||
process.env.RENDERER_REMOTE_DEBUGGING = true
|
||||
}
|
||||
|
||||
// Define exit code for relaunch and set it in the environment
|
||||
const relaunchExitCode = 69
|
||||
process.env.FREETUBE_RELAUNCH_EXIT_CODE = relaunchExitCode
|
||||
|
||||
const port = 9080
|
||||
|
||||
async function killElectron(pid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pid) {
|
||||
kill(pid, err => {
|
||||
if (err) reject(err)
|
||||
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function restartElectron() {
|
||||
console.log('\nStarting electron...')
|
||||
|
||||
const { pid } = electronProcess || {}
|
||||
await killElectron(pid)
|
||||
|
||||
electronProcess = spawn(electron, [
|
||||
path.join(__dirname, '../dist/main.js'),
|
||||
// '--enable-logging', // Enable to show logs from all electron processes
|
||||
remoteDebugging ? '--inspect=9222' : '',
|
||||
remoteDebugging ? '--remote-debugging-port=9223' : ''
|
||||
],
|
||||
// { stdio: 'inherit' } // required for logs to actually appear in the stdout
|
||||
)
|
||||
|
||||
electronProcess.on('exit', (code, _) => {
|
||||
if (code === relaunchExitCode) {
|
||||
electronProcess = null
|
||||
restartElectron()
|
||||
return
|
||||
}
|
||||
|
||||
if (!manualRestart) process.exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('webpack').Compiler} compiler
|
||||
* @param {WebpackDevServer} devServer
|
||||
*/
|
||||
function setupNotifyLocaleUpdate(compiler, devServer) {
|
||||
const notifyLocaleChange = (updatedLocales) => {
|
||||
devServer.sendMessage(devServer.webSocketServer.clients, 'freetube-locale-update', updatedLocales)
|
||||
}
|
||||
|
||||
compiler.options.plugins
|
||||
.filter(plugin => plugin instanceof ProcessLocalesPlugin)
|
||||
.forEach((/** @type {ProcessLocalesPlugin} */plugin) => {
|
||||
plugin.notifyLocaleChange = notifyLocaleChange
|
||||
})
|
||||
}
|
||||
|
||||
function startBotGuardScript() {
|
||||
webpack(botGuardScriptConfig, (err) => {
|
||||
if (err) console.error(err)
|
||||
|
||||
console.log(`\nCompiled ${botGuardScriptConfig.name} script!`)
|
||||
})
|
||||
}
|
||||
|
||||
function startMain() {
|
||||
const compiler = webpack(mainConfig)
|
||||
const { name } = compiler
|
||||
|
||||
compiler.hooks.afterEmit.tap('afterEmit', async () => {
|
||||
console.log(`\nCompiled ${name} script!`)
|
||||
|
||||
manualRestart = true
|
||||
await restartElectron()
|
||||
setTimeout(() => {
|
||||
manualRestart = false
|
||||
}, 2500)
|
||||
|
||||
console.log(`\nWatching file changes for ${name} script...`)
|
||||
})
|
||||
|
||||
compiler.watch({
|
||||
aggregateTimeout: 500,
|
||||
},
|
||||
err => {
|
||||
if (err) console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function startRenderer(callback) {
|
||||
const compiler = webpack(rendererConfig)
|
||||
const { name } = compiler
|
||||
|
||||
compiler.hooks.afterEmit.tap('afterEmit', () => {
|
||||
console.log(`\nCompiled ${name} script!`)
|
||||
console.log(`\nWatching file changes for ${name} script...`)
|
||||
})
|
||||
|
||||
const server = new WebpackDevServer({
|
||||
static: [
|
||||
{
|
||||
directory: path.resolve(__dirname, '..', 'static'),
|
||||
watch: {
|
||||
ignored: [
|
||||
/(dashFiles|storyboards)\/*/,
|
||||
'/**/.DS_Store',
|
||||
'**/static/locales/*'
|
||||
]
|
||||
},
|
||||
publicPath: '/static'
|
||||
},
|
||||
{
|
||||
directory: path.resolve(__dirname, '..', 'node_modules', 'shaka-player', 'ui', 'locales'),
|
||||
publicPath: '/static/shaka-player-locales',
|
||||
watch: {
|
||||
// Ignore everything that isn't one of the locales that we would bundle in production mode
|
||||
ignored: `**/!(${SHAKA_LOCALES_TO_BE_BUNDLED.join('|')}).json`
|
||||
}
|
||||
}
|
||||
],
|
||||
port
|
||||
}, compiler)
|
||||
|
||||
server.startCallback(err => {
|
||||
if (err) console.error(err)
|
||||
|
||||
setupNotifyLocaleUpdate(compiler, server)
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
function startWeb () {
|
||||
const compiler = webpack(webConfig)
|
||||
const { name } = compiler
|
||||
|
||||
compiler.hooks.afterEmit.tap('afterEmit', () => {
|
||||
console.log(`\nCompiled ${name} script!`)
|
||||
console.log(`\nWatching file changes for ${name} script...`)
|
||||
})
|
||||
|
||||
const server = new WebpackDevServer({
|
||||
open: true,
|
||||
static: {
|
||||
directory: path.resolve(__dirname, '..', 'static'),
|
||||
watch: {
|
||||
ignored: [
|
||||
/(dashFiles|storyboards)\/*/,
|
||||
'/**/.DS_Store',
|
||||
'**/static/locales/*'
|
||||
]
|
||||
}
|
||||
},
|
||||
port
|
||||
}, compiler)
|
||||
|
||||
server.startCallback(err => {
|
||||
if (err) console.error(err)
|
||||
|
||||
setupNotifyLocaleUpdate(compiler, server)
|
||||
})
|
||||
}
|
||||
if (!web) {
|
||||
startRenderer(() => {
|
||||
startBotGuardScript()
|
||||
startMain()
|
||||
})
|
||||
} else {
|
||||
startWeb()
|
||||
}
|
||||
94
_scripts/ebuilder.config.js
Normal file
94
_scripts/ebuilder.config.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
const { name, productName } = require('../package.json')
|
||||
|
||||
const config = {
|
||||
appId: `io.freetubeapp.${name}`,
|
||||
copyright: 'Copyleft © 2020-2025 freetubeapp@protonmail.com',
|
||||
// asar: false,
|
||||
// compression: 'store',
|
||||
productName,
|
||||
directories: {
|
||||
output: './build/',
|
||||
},
|
||||
protocols: [
|
||||
{
|
||||
name: 'FreeTube',
|
||||
schemes: [
|
||||
'freetube'
|
||||
]
|
||||
}
|
||||
],
|
||||
files: [
|
||||
'_icons/iconColor.*',
|
||||
'icon.svg',
|
||||
'./dist/**/*',
|
||||
'!dist/web/*',
|
||||
'!node_modules/**/*',
|
||||
],
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
path: '/Applications',
|
||||
type: 'link',
|
||||
x: 410,
|
||||
y: 230,
|
||||
},
|
||||
{
|
||||
type: 'file',
|
||||
x: 130,
|
||||
y: 230,
|
||||
},
|
||||
],
|
||||
window: {
|
||||
height: 380,
|
||||
width: 540,
|
||||
}
|
||||
},
|
||||
linux: {
|
||||
category: 'Network',
|
||||
icon: '_icons/icon.svg',
|
||||
target: ['deb', 'zip', '7z', 'rpm', 'AppImage', 'pacman'],
|
||||
},
|
||||
// See the following issues for more information
|
||||
// https://github.com/jordansissel/fpm/issues/1503
|
||||
// https://github.com/jgraph/drawio-desktop/issues/259
|
||||
rpm: {
|
||||
fpm: ['--rpm-rpmbuild-define=_build_id_links none']
|
||||
},
|
||||
deb: {
|
||||
depends: [
|
||||
'libgtk-3-0',
|
||||
'libnotify4',
|
||||
'libnss3',
|
||||
'libxss1',
|
||||
'libxtst6',
|
||||
'xdg-utils',
|
||||
'libatspi2.0-0',
|
||||
'libuuid1',
|
||||
'libsecret-1-0'
|
||||
]
|
||||
},
|
||||
mac: {
|
||||
category: 'public.app-category.utilities',
|
||||
icon: '_icons/iconMac.icns',
|
||||
target: ['dmg', 'zip', '7z'],
|
||||
type: 'distribution',
|
||||
extendInfo: {
|
||||
CFBundleURLTypes: [
|
||||
'freetube'
|
||||
],
|
||||
CFBundleURLSchemes: [
|
||||
'freetube'
|
||||
]
|
||||
}
|
||||
},
|
||||
win: {
|
||||
icon: '_icons/icon.ico',
|
||||
target: ['nsis', 'zip', '7z', 'portable'],
|
||||
},
|
||||
nsis: {
|
||||
allowToChangeInstallationDirectory: true,
|
||||
oneClick: false,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
11
_scripts/eslint-rules/plugin.mjs
Normal file
11
_scripts/eslint-rules/plugin.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import preferUseI18nPolyfillRule from './prefer-use-i18n-polyfill-rule.mjs'
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
name: 'eslint-plugin-freetube',
|
||||
version: '1.0'
|
||||
},
|
||||
rules: {
|
||||
'prefer-use-i18n-polyfill': preferUseI18nPolyfillRule
|
||||
}
|
||||
}
|
||||
62
_scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs
Normal file
62
_scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { dirname, relative, resolve } from 'path'
|
||||
|
||||
const polyfillPath = resolve(import.meta.dirname, '../../src/renderer/composables/use-i18n-polyfill')
|
||||
|
||||
function getRelativePolyfillPath(filePath) {
|
||||
const relativePath = relative(dirname(filePath), polyfillPath).replaceAll('\\', '/')
|
||||
|
||||
if (relativePath[0] !== '.') {
|
||||
return `./${relativePath}`
|
||||
}
|
||||
|
||||
return relativePath
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: 'code'
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
'ImportDeclaration[source.value="vue-i18n"]'(node) {
|
||||
const specifierIndex = node.specifiers.findIndex(specifier => specifier.type === 'ImportSpecifier' && specifier.imported.name === 'useI18n')
|
||||
|
||||
if (specifierIndex !== -1) {
|
||||
context.report({
|
||||
node: node.specifiers.length === 1 ? node : node.specifiers[specifierIndex],
|
||||
message: "Please use FreeTube's useI18n polyfill, as vue-i18n's useI18n composable does not work when the vue-i18n is in legacy mode, which is needed for components using the Options API.",
|
||||
fix: context.physicalFilename === '<text>'
|
||||
? undefined
|
||||
: (fixer) => {
|
||||
const relativePath = getRelativePolyfillPath(context.physicalFilename)
|
||||
|
||||
// If the import only imports `useI18n`, we can just update the source/from text
|
||||
// Else we need to create a new import for `useI18n` and remove useI18n from the original one
|
||||
if (node.specifiers.length === 1) {
|
||||
return fixer.replaceText(node.source, `'${relativePath}'`)
|
||||
} else {
|
||||
const specifier = node.specifiers[specifierIndex]
|
||||
|
||||
let specifierText = 'useI18n'
|
||||
|
||||
if (specifier.imported.name !== specifier.local.name) {
|
||||
specifierText += ` as ${specifier.local.name}`
|
||||
}
|
||||
|
||||
return [
|
||||
fixer.removeRange([
|
||||
specifierIndex === 0 ? specifier.start : node.specifiers[specifierIndex - 1].end,
|
||||
specifierIndex === node.specifiers.length - 1 ? specifier.end : node.specifiers[specifierIndex + 1].start
|
||||
]),
|
||||
fixer.insertTextAfter(node, `\nimport { ${specifierText} } from '${relativePath}'`)
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
_scripts/getInstances.js
Normal file
16
_scripts/getInstances.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const fs = require('fs/promises')
|
||||
const invidiousApiUrl = 'https://api.invidious.io/instances.json'
|
||||
|
||||
fetch(invidiousApiUrl).then(e => e.json()).then(res => {
|
||||
const data = res.filter((instance) => {
|
||||
return !(instance[0].includes('.onion') ||
|
||||
instance[0].includes('.i2p') ||
|
||||
!instance[1].api)
|
||||
}).map((instance) => {
|
||||
return {
|
||||
url: instance[1].uri.replace(/\/$/, ''),
|
||||
cors: instance[1].cors
|
||||
}
|
||||
})
|
||||
fs.writeFile('././static/invidious-instances.json', JSON.stringify(data, null, 2))
|
||||
})
|
||||
162
_scripts/getRegions.mjs
Normal file
162
_scripts/getRegions.mjs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* This script updates the files in static/geolocations with the available locations on YouTube.
|
||||
*
|
||||
* It tries to map every active FreeTube language (static/locales/activelocales.json)
|
||||
* to it's equivalent on YouTube.
|
||||
*
|
||||
* It then uses those language mappings,
|
||||
* to scrape the location selection menu on the YouTube website, in every mapped language.
|
||||
*
|
||||
* All languages it couldn't find on YouTube, that don't have manually added mapping,
|
||||
* get logged to the console, as well as all unmapped YouTube languages.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
||||
import { Innertube, Misc } from 'youtubei.js'
|
||||
|
||||
const STATIC_DIRECTORY = `${import.meta.dirname}/../static`
|
||||
|
||||
const activeLanguagesPath = `${STATIC_DIRECTORY}/locales/activeLocales.json`
|
||||
/** @type {string[]} */
|
||||
const activeLanguages = JSON.parse(readFileSync(activeLanguagesPath, { encoding: 'utf8' }))
|
||||
|
||||
// en-US is en on YouTube
|
||||
const initialResponse = await scrapeLanguage('en')
|
||||
|
||||
// Scrape language menu in en-US
|
||||
|
||||
/** @type {string[]} */
|
||||
const youTubeLanguages = initialResponse.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items[2].compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items
|
||||
.map(({ compactLinkRenderer }) => {
|
||||
return compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].selectLanguageCommand.hl
|
||||
})
|
||||
|
||||
// map FreeTube languages to their YouTube equivalents
|
||||
|
||||
const foundLanguageNames = ['en-US']
|
||||
const unusedYouTubeLanguageNames = []
|
||||
const languagesToScrape = []
|
||||
|
||||
for (const language of youTubeLanguages) {
|
||||
if (activeLanguages.includes(language)) {
|
||||
foundLanguageNames.push(language)
|
||||
languagesToScrape.push({
|
||||
youTube: language,
|
||||
freeTube: language
|
||||
})
|
||||
// eslint-disable-next-line @stylistic/brace-style
|
||||
}
|
||||
// special cases
|
||||
else if (language === 'de') {
|
||||
foundLanguageNames.push('de-DE')
|
||||
languagesToScrape.push({
|
||||
youTube: 'de',
|
||||
freeTube: 'de-DE'
|
||||
})
|
||||
} else if (language === 'fr') {
|
||||
foundLanguageNames.push('fr-FR')
|
||||
languagesToScrape.push({
|
||||
youTube: 'fr',
|
||||
freeTube: 'fr-FR'
|
||||
})
|
||||
} else if (language === 'no') {
|
||||
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
// "no" is the macro language for "nb" and "nn"
|
||||
foundLanguageNames.push('nb-NO', 'nn')
|
||||
languagesToScrape.push({
|
||||
youTube: 'no',
|
||||
freeTube: 'nb-NO'
|
||||
})
|
||||
languagesToScrape.push({
|
||||
youTube: 'no',
|
||||
freeTube: 'nn'
|
||||
})
|
||||
} else if (language === 'iw') {
|
||||
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
// "iw" is the old/original code for Hebrew, these days it's "he"
|
||||
foundLanguageNames.push('he')
|
||||
languagesToScrape.push({
|
||||
youTube: 'iw',
|
||||
freeTube: 'he'
|
||||
})
|
||||
} else if (language === 'es-419') {
|
||||
foundLanguageNames.push('es-AR', 'es-MX')
|
||||
languagesToScrape.push({
|
||||
youTube: 'es-419',
|
||||
freeTube: 'es-AR'
|
||||
})
|
||||
languagesToScrape.push({
|
||||
youTube: 'es-419',
|
||||
freeTube: 'es-MX'
|
||||
})
|
||||
} else if (language !== 'en') {
|
||||
unusedYouTubeLanguageNames.push(language)
|
||||
}
|
||||
}
|
||||
|
||||
foundLanguageNames.push('pt-BR')
|
||||
languagesToScrape.push({
|
||||
youTube: 'pt',
|
||||
freeTube: 'pt-BR'
|
||||
})
|
||||
|
||||
console.log("Active FreeTube languages that aren't available on YouTube:")
|
||||
console.log(activeLanguages.filter(lang => !foundLanguageNames.includes(lang)).sort())
|
||||
|
||||
console.log("YouTube languages that don't have an equivalent active FreeTube language:")
|
||||
console.log(unusedYouTubeLanguageNames.sort())
|
||||
|
||||
// Scrape the location menu in various languages and write files to the file system
|
||||
|
||||
rmSync(`${STATIC_DIRECTORY}/geolocations`, { recursive: true })
|
||||
mkdirSync(`${STATIC_DIRECTORY}/geolocations`)
|
||||
|
||||
processGeolocations('en-US', 'en', initialResponse)
|
||||
|
||||
for (const { youTube, freeTube } of languagesToScrape) {
|
||||
const response = await scrapeLanguage(youTube)
|
||||
|
||||
processGeolocations(freeTube, youTube, response)
|
||||
}
|
||||
|
||||
async function scrapeLanguage(youTubeLanguageCode) {
|
||||
const session = await Innertube.create({
|
||||
retrieve_player: false,
|
||||
generate_session_locally: true,
|
||||
lang: youTubeLanguageCode
|
||||
})
|
||||
|
||||
return await session.actions.execute('/account/account_menu')
|
||||
}
|
||||
|
||||
function processGeolocations(freeTubeLanguage, youTubeLanguage, response) {
|
||||
const geolocations = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items[4].compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items
|
||||
.map(({ compactLinkRenderer }) => {
|
||||
return {
|
||||
name: new Misc.Text(compactLinkRenderer.title).toString().trim(),
|
||||
code: compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].selectCountryCommand.gl
|
||||
}
|
||||
})
|
||||
|
||||
const normalisedFreeTubeLanguage = freeTubeLanguage.replace('_', '-')
|
||||
|
||||
// give Intl.Collator 4 locales, in the hopes that it supports one of them
|
||||
// deduplicate the list so it doesn't have to do duplicate work
|
||||
const localeSet = new Set()
|
||||
localeSet.add(normalisedFreeTubeLanguage)
|
||||
localeSet.add(youTubeLanguage)
|
||||
localeSet.add(normalisedFreeTubeLanguage.split('-')[0])
|
||||
localeSet.add(youTubeLanguage.split('-')[0])
|
||||
|
||||
const locales = Array.from(localeSet)
|
||||
|
||||
// only sort if node supports sorting the language, otherwise hope that YouTube's sorting was correct
|
||||
// node 20.3.1 doesn't support sorting `eu`
|
||||
if (Intl.Collator.supportedLocalesOf(locales).length > 0) {
|
||||
const collator = new Intl.Collator(locales)
|
||||
|
||||
geolocations.sort((a, b) => collator.compare(a.name, b.name))
|
||||
}
|
||||
|
||||
writeFileSync(`${STATIC_DIRECTORY}/geolocations/${freeTubeLanguage}.json`, JSON.stringify(geolocations))
|
||||
}
|
||||
109
_scripts/getShakaLocales.js
Normal file
109
_scripts/getShakaLocales.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
const { readFileSync, readdirSync } = require('fs')
|
||||
|
||||
function getPreloadedLocales() {
|
||||
const localesFile = readFileSync(`${__dirname}/../node_modules/shaka-player/dist/locales.js`, 'utf-8')
|
||||
|
||||
const localesLine = localesFile.match(/^\/\/ LOCALES: ([\w ,-]+)$/m)
|
||||
|
||||
if (!localesLine) {
|
||||
throw new Error("Failed to parse shaka-player's preloaded locales")
|
||||
}
|
||||
|
||||
return localesLine[1].split(',').map(locale => locale.trim())
|
||||
}
|
||||
|
||||
function getAllLocales() {
|
||||
const filenames = readdirSync(`${__dirname}/../node_modules/shaka-player/ui/locales`)
|
||||
|
||||
return new Set(filenames
|
||||
.filter(filename => filename !== 'source.json' && filename.endsWith('.json'))
|
||||
.map(filename => filename.replace('.json', '')))
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the shaka locales to FreeTube's active ones
|
||||
* This allows us to know which locale files are actually needed
|
||||
* and which shaka locale needs to be activated for a given FreeTube one.
|
||||
* @param {Set<string>} shakaLocales
|
||||
* @param {string[]} freeTubeLocales
|
||||
*/
|
||||
function getMappings(shakaLocales, freeTubeLocales) {
|
||||
/**
|
||||
* @type {[string, string][]}
|
||||
* Using this structure as it gets passed to `new Map()` in the player component
|
||||
* The first element is the FreeTube locale, the second one is the shaka-player one
|
||||
*/
|
||||
const mappings = []
|
||||
|
||||
for (const locale of freeTubeLocales) {
|
||||
if (shakaLocales.has(locale)) {
|
||||
mappings.push([
|
||||
locale,
|
||||
locale
|
||||
])
|
||||
} else if (shakaLocales.has(locale.split('-')[0])) {
|
||||
mappings.push([
|
||||
locale,
|
||||
locale.split('-')[0]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// special cases
|
||||
|
||||
mappings.push(
|
||||
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
// "no" is the macro language for "nb" and "nn"
|
||||
[
|
||||
'nb-NO',
|
||||
'no'
|
||||
],
|
||||
[
|
||||
'nn',
|
||||
'no'
|
||||
],
|
||||
|
||||
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
// "iw" is the old/original code for Hebrew, these days it's "he"
|
||||
[
|
||||
'he',
|
||||
'iw'
|
||||
],
|
||||
|
||||
// not sure why we have pt, pt-PT and pt-BR in the FreeTube locales
|
||||
// as pt and pt-PT are the same thing, but we should handle it here anyway
|
||||
[
|
||||
'pt',
|
||||
'pt-PT'
|
||||
]
|
||||
)
|
||||
|
||||
return mappings
|
||||
}
|
||||
|
||||
function getShakaLocales() {
|
||||
const shakaLocales = getAllLocales()
|
||||
|
||||
/** @type {string[]} */
|
||||
const freeTubeLocales = JSON.parse(readFileSync(`${__dirname}/../static/locales/activeLocales.json`, 'utf-8'))
|
||||
|
||||
const mappings = getMappings(shakaLocales, freeTubeLocales)
|
||||
|
||||
const preloaded = getPreloadedLocales()
|
||||
|
||||
const shakaMappings = mappings.map(mapping => mapping[1])
|
||||
|
||||
// use a set to deduplicate the list
|
||||
// we don't need to bundle any locale files that are already embedded in shaka-player/preloaded
|
||||
|
||||
/** @type {string[]} */
|
||||
const toBeBundled = [...new Set(shakaMappings.filter(locale => !preloaded.includes(locale)))]
|
||||
|
||||
return {
|
||||
SHAKA_LOCALE_MAPPINGS: mappings,
|
||||
SHAKA_LOCALES_PREBUNDLED: preloaded,
|
||||
SHAKA_LOCALES_TO_BE_BUNDLED: toBeBundled
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = getShakaLocales()
|
||||
32
_scripts/helpers.js
Normal file
32
_scripts/helpers.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
const exec = require('child_process').exec
|
||||
|
||||
/**
|
||||
* Calls `child_process`.exec, but it outputs
|
||||
* all of the stdout live and can be awaited
|
||||
* @param {string} command The command to be executed
|
||||
* @returns
|
||||
*/
|
||||
function execWithLiveOutput (command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const execCall = exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
execCall.stdout.on('data', (data) => {
|
||||
process.stdout.write(data)
|
||||
})
|
||||
execCall.stderr.on('data', (data) => {
|
||||
console.error(data)
|
||||
})
|
||||
execCall.on('close', () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
execWithLiveOutput
|
||||
}
|
||||
48
_scripts/injectAllowedPaths.mjs
Normal file
48
_scripts/injectAllowedPaths.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Injects the paths that the renderer process is allowed to read into the main.js file,
|
||||
* by replacing __FREETUBE_ALLOWED_PATHS__ with an array of strings with the paths.
|
||||
*
|
||||
* This allows the main process to validate the paths which the renderer process accesses,
|
||||
* to ensure that it cannot access other files on the disk, without the users permission (e.g. file picker).
|
||||
*/
|
||||
import { closeSync, ftruncateSync, openSync, readFileSync, readdirSync, writeSync } from 'fs'
|
||||
import { join, relative, resolve } from 'path'
|
||||
|
||||
const distDirectory = resolve(import.meta.dirname, '..', 'dist')
|
||||
const webDirectory = join(distDirectory, 'web')
|
||||
|
||||
const paths = readdirSync(distDirectory, {
|
||||
recursive: true,
|
||||
withFileTypes: true
|
||||
})
|
||||
.filter(dirent => {
|
||||
// only include files not directories
|
||||
return dirent.isFile() &&
|
||||
// disallow the renderer process/browser windows to read the main.js file
|
||||
dirent.name !== 'main.js' &&
|
||||
dirent.name !== 'main.js.LICENSE.txt' &&
|
||||
// disallow the renderer process/browser windows to read the botGuardScript.js file
|
||||
dirent.name !== 'botGuardScript.js' &&
|
||||
// filter out any web build files, in case the dist directory contains a web build
|
||||
!dirent.parentPath.startsWith(webDirectory)
|
||||
})
|
||||
.map(dirent => {
|
||||
const joined = join(dirent.parentPath, dirent.name)
|
||||
return '/' + relative(distDirectory, joined).replaceAll('\\', '/')
|
||||
})
|
||||
|
||||
let fileHandle
|
||||
try {
|
||||
fileHandle = openSync(join(distDirectory, 'main.js'), 'r+')
|
||||
|
||||
let contents = readFileSync(fileHandle, 'utf-8')
|
||||
|
||||
contents = contents.replace('__FREETUBE_ALLOWED_PATHS__', JSON.stringify(paths))
|
||||
|
||||
ftruncateSync(fileHandle)
|
||||
writeSync(fileHandle, contents, 0, 'utf-8')
|
||||
} finally {
|
||||
if (typeof fileHandle !== 'undefined') {
|
||||
closeSync(fileHandle)
|
||||
}
|
||||
}
|
||||
22
_scripts/mime-db-shrinking-loader.js
Normal file
22
_scripts/mime-db-shrinking-loader.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* electron-context-menu only needs mime-db for its save as feature.
|
||||
* As we only activate save image and save as image features, we can remove all other mimetypes,
|
||||
* as they will never get used.
|
||||
* Which results in quite a significant reduction in file size.
|
||||
* @param {string} source
|
||||
*/
|
||||
module.exports = function (source) {
|
||||
const original = JSON.parse(source)
|
||||
|
||||
// Only the extensions field is needed, see: https://github.com/kevva/ext-list/blob/v2.2.2/index.js
|
||||
|
||||
return JSON.stringify({
|
||||
'image/apng': { extensions: original['image/apng'].extensions },
|
||||
'image/avif': { extensions: original['image/avif'].extensions },
|
||||
'image/gif': { extensions: original['image/gif'].extensions },
|
||||
'image/jpeg': { extensions: original['image/jpeg'].extensions },
|
||||
'image/png': { extensions: original['image/png'].extensions },
|
||||
'image/svg+xml': { extensions: original['image/svg+xml'].extensions },
|
||||
'image/webp': { extensions: original['image/webp'].extensions }
|
||||
})
|
||||
}
|
||||
134
_scripts/patchShaka.mjs
Normal file
134
_scripts/patchShaka.mjs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// This script fixes shaka not exporting its type definitions and referencing remote google fonts in its CSS
|
||||
// by adding an export line to the type definitions and downloading the fonts and updating the CSS to point to the local files
|
||||
// this script only makes changes if they are needed, so running it multiple times doesn't cause any problems
|
||||
|
||||
import { appendFileSync, closeSync, ftruncateSync, openSync, readFileSync, writeFileSync, writeSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const SHAKA_DIST_DIR = resolve(import.meta.dirname, '../node_modules/shaka-player/dist')
|
||||
|
||||
function fixTypes() {
|
||||
let fixedTypes = false
|
||||
|
||||
let fileHandleNormal
|
||||
try {
|
||||
fileHandleNormal = openSync(`${SHAKA_DIST_DIR}/shaka-player.ui.d.ts`, 'a+')
|
||||
|
||||
const contents = readFileSync(fileHandleNormal, 'utf-8')
|
||||
|
||||
// This script is run after every `yarn install`, even if shaka-player wasn't updated
|
||||
// So we want to check first, if we actually need to make any changes
|
||||
// or if the ones from the previous run are still intact
|
||||
if (!contents.includes('export default shaka')) {
|
||||
appendFileSync(fileHandleNormal, 'export default shaka;\n')
|
||||
|
||||
fixedTypes = true
|
||||
}
|
||||
} finally {
|
||||
if (typeof fileHandleNormal !== 'undefined') {
|
||||
closeSync(fileHandleNormal)
|
||||
}
|
||||
}
|
||||
|
||||
let fileHandleDebug
|
||||
try {
|
||||
fileHandleDebug = openSync(`${SHAKA_DIST_DIR}/shaka-player.ui.debug.d.ts`, 'a+')
|
||||
|
||||
const contents = readFileSync(fileHandleDebug, 'utf-8')
|
||||
|
||||
// This script is run after every `yarn install`, even if shaka-player wasn't updated
|
||||
// So we want to check first, if we actually need to make any changes
|
||||
// or if the ones from the previous run are still intact
|
||||
if (!contents.includes('export default shaka')) {
|
||||
appendFileSync(fileHandleDebug, 'export default shaka;\n')
|
||||
|
||||
fixedTypes = true
|
||||
}
|
||||
} finally {
|
||||
if (typeof fileHandleDebug !== 'undefined') {
|
||||
closeSync(fileHandleDebug)
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedTypes) {
|
||||
console.log('Fixed shaka-player types')
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRobotoFont() {
|
||||
let cssFileHandle
|
||||
try {
|
||||
cssFileHandle = openSync(`${SHAKA_DIST_DIR}/controls.css`, 'r+')
|
||||
|
||||
let cssContents = readFileSync(cssFileHandle, 'utf-8')
|
||||
|
||||
const beforeReplacement = cssContents.length
|
||||
cssContents = cssContents.replace(/@font-face{font-family:Roboto;[^}]+}/, '')
|
||||
|
||||
if (cssContents.length !== beforeReplacement) {
|
||||
ftruncateSync(cssFileHandle)
|
||||
writeSync(cssFileHandle, cssContents, 0, 'utf-8')
|
||||
|
||||
console.log('Removed shaka-player Roboto font, so it uses ours')
|
||||
}
|
||||
} finally {
|
||||
if (typeof cssFileHandle !== 'undefined') {
|
||||
closeSync(cssFileHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceAndDownloadMaterialIconsFont() {
|
||||
let cssFileHandle
|
||||
try {
|
||||
cssFileHandle = openSync(`${SHAKA_DIST_DIR}/controls.css`, 'r+')
|
||||
|
||||
let cssContents = readFileSync(cssFileHandle, 'utf-8')
|
||||
|
||||
const fontFaceRegex = /@font-face{font-family:'Material Icons Round'[^}]+format\('opentype'\)}/
|
||||
|
||||
if (fontFaceRegex.test(cssContents)) {
|
||||
const cssResponse = await fetch('https://fonts.googleapis.com/icon?family=Material+Icons+Round', {
|
||||
headers: {
|
||||
// Without the user-agent it returns the otf file instead of the woff2 one
|
||||
'user-agent': 'Firefox/125.0'
|
||||
}
|
||||
})
|
||||
|
||||
const text = await cssResponse.text()
|
||||
|
||||
let newFontCSS = text.match(/(@font-face\s*{[^}]+})/)[1].replaceAll('\n', '')
|
||||
|
||||
const urlMatch = newFontCSS.match(/https:\/\/fonts\.gstatic\.com\/s\/materialiconsround\/(?<version>[^/]+)\/[^.]+\.(?<extension>\w+)/)
|
||||
|
||||
const url = urlMatch[0]
|
||||
const { version, extension } = urlMatch.groups
|
||||
|
||||
const fontResponse = await fetch(url)
|
||||
const fontContent = new Uint8Array(await fontResponse.arrayBuffer())
|
||||
|
||||
const filename = `shaka-materialiconsround-${version}.${extension}`
|
||||
writeFileSync(`${SHAKA_DIST_DIR}/${filename}`, fontContent)
|
||||
|
||||
newFontCSS = newFontCSS.replace(url, `./${filename}`)
|
||||
|
||||
cssContents = cssContents.replace(fontFaceRegex, newFontCSS)
|
||||
|
||||
ftruncateSync(cssFileHandle)
|
||||
writeSync(cssFileHandle, cssContents, 0, 'utf-8')
|
||||
|
||||
console.log('Changed shaka-player Material Icons Rounded font to use the smaller woff2 format instead of otf')
|
||||
console.log('Downloaded shaka-player Material Icons Rounded font')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
if (typeof cssFileHandle !== 'undefined') {
|
||||
closeSync(cssFileHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fixTypes()
|
||||
await removeRobotoFont()
|
||||
await replaceAndDownloadMaterialIconsFont()
|
||||
105
_scripts/releaseAndroid.js
Normal file
105
_scripts/releaseAndroid.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// script to trigger release
|
||||
// #region Imports
|
||||
const { version } = require('../package.json')
|
||||
const { promisify } = require('util')
|
||||
const { fetch } = require('undici')
|
||||
const { get } = require('prompt')
|
||||
const { exec } = require('child_process')
|
||||
const { randomUUID } = require('crypto')
|
||||
const { writeFile, rm, readFile } = require('fs/promises')
|
||||
// #endregion
|
||||
;(async (get, exec) => {
|
||||
const givenArguments = { dryRun: false }
|
||||
for (let arg of process.argv) {
|
||||
if (arg.startsWith('--version-number=')) {
|
||||
givenArguments.versionNumber = arg.split('--version-number=')[1]
|
||||
}
|
||||
if (arg.startsWith('--number-of-commits=')) {
|
||||
givenArguments.numberOfCommits = parseInt(arg.split('--number-of-commits=')[1])
|
||||
}
|
||||
if (arg === '--dry-run') {
|
||||
givenArguments.dryRun = true
|
||||
}
|
||||
if (arg.startsWith('--f=')) {
|
||||
givenArguments.message = arg.split('--f=')[1]
|
||||
}
|
||||
}
|
||||
if (givenArguments.dryRun) {
|
||||
console.log('starting dry run')
|
||||
}
|
||||
const releases = await fetch('https://api.github.com/repos/MarmadileManteater/FreeTubeCordova/releases')
|
||||
/** @type {Array<{tag_name: string}>} */
|
||||
const releasesJSON = await releases.json()
|
||||
// #region Version Number
|
||||
const latestTag = releasesJSON[0].tag_name
|
||||
const [ _latestMajor, _latestMinor, _latestPatch, latestRun ] = latestTag.split('.')
|
||||
const [ currentMajor, currentMinor, currentPatch ] = version.split('.')
|
||||
const latestRunNumber = parseInt(latestRun)
|
||||
const currentRunNumber = latestRunNumber + 1
|
||||
const defaultVersionNumber = `${currentMajor}.${currentMinor}.${currentPatch}.${currentRunNumber}`
|
||||
// get version number from either the props or a prompt
|
||||
let { versionNumber } = ('versionNumber' in givenArguments) ? givenArguments : await get({
|
||||
properties: {
|
||||
versionNumber: {
|
||||
pattern: /[0-9]*?.[0-9]*?.[0-9]*?.[0-9]*/,
|
||||
message: 'Version number to release with',
|
||||
default: defaultVersionNumber
|
||||
}
|
||||
}
|
||||
})
|
||||
const buildNumber = versionNumber.split('.').at(-1)
|
||||
if (versionNumber === 'default') {
|
||||
versionNumber = defaultVersionNumber
|
||||
}
|
||||
// #endregion
|
||||
if (!('message' in givenArguments)) {
|
||||
// #region Latest Git History
|
||||
const gitDiff = (await exec('git log origin/development...origin/release')).stdout
|
||||
const numberOfCommits = ('numberOfCommits' in givenArguments) ? givenArguments.numberOfCommits : 3
|
||||
let accumulator = 0
|
||||
let mostRecentCommits = ''
|
||||
for (let line of gitDiff.split('\n')) {
|
||||
if (line.trim() === '') {
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('commit ')) {
|
||||
mostRecentCommits += `\n`
|
||||
accumulator++
|
||||
}
|
||||
if (accumulator < numberOfCommits + 1) {
|
||||
mostRecentCommits += `${line}\n`
|
||||
}
|
||||
}
|
||||
mostRecentCommits = mostRecentCommits.trim()
|
||||
// #endregion
|
||||
givenArguments.message = mostRecentCommits
|
||||
} else {
|
||||
// message is in given arguments (a file was passed that needs to be read)
|
||||
givenArguments.message = await readFile(givenArguments.message)
|
||||
}
|
||||
const commitMessage = `**Release ${versionNumber}**
|
||||
|
||||
${givenArguments.message}
|
||||
|
||||
...
|
||||
|
||||
**Full Changelog**: https://github.com/MarmadileManteater/FreeTubeCordova/compare/${latestTag}...${versionNumber}`
|
||||
// #region Commit changes to release branch
|
||||
if (!givenArguments.dryRun) {
|
||||
const fileId = randomUUID()
|
||||
await writeFile(`commit_message_${fileId}.txt`, commitMessage)
|
||||
await exec('git pull')
|
||||
await exec('git checkout release')
|
||||
await exec('git merge origin/development --no-commit --no-ff')
|
||||
await exec(`git commit -F commit_message_${fileId}.txt`)
|
||||
await rm(`commit_message_${fileId}.txt`)
|
||||
} else {
|
||||
// is a dry run
|
||||
console.log(commitMessage)
|
||||
console.log('------')
|
||||
console.log('nothing committed: dry run')
|
||||
}
|
||||
// #endregion
|
||||
|
||||
})(promisify(get), promisify(exec))
|
||||
|
||||
13
_scripts/sigFrameConfig.js
Normal file
13
_scripts/sigFrameConfig.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const { hash } = require('crypto')
|
||||
const { join } = require('path')
|
||||
const { readFileSync } = require('fs')
|
||||
|
||||
const path = join(__dirname, '../src/renderer/sigFrameScript.js')
|
||||
const rawScript = readFileSync(path, 'utf8')
|
||||
|
||||
const script = rawScript.split(/\r?\n/).map(line => line.trim()).filter(line => !line.startsWith('//')).join('')
|
||||
|
||||
module.exports.sigFrameTemplateParameters = {
|
||||
sigFrameSrc: `data:text/html,${encodeURIComponent(`<!doctype html><script>${script}</script>`)}`,
|
||||
sigFrameCspHash: `sha512-${hash('sha512', script, 'base64')}`
|
||||
}
|
||||
15
_scripts/sigViewConfig.js
Normal file
15
_scripts/sigViewConfig.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const { hash } = require('crypto')
|
||||
const { join } = require('path')
|
||||
const { readFileSync } = require('fs')
|
||||
|
||||
const path = join(__dirname, '../src/renderer/sigViewScript.js')
|
||||
const rawScript = readFileSync(path, 'utf8')
|
||||
|
||||
const script = process.env.NODE_ENV === 'development'
|
||||
? rawScript
|
||||
: require('terser').minify_sync({ [path]: rawScript }).code
|
||||
|
||||
module.exports.sigViewTemplateParameters = {
|
||||
sigViewRaw: `<!doctype html><script>${script}</script>`,
|
||||
sigViewCspHash: `sha512-${hash('sha512', script, 'base64')}`
|
||||
}
|
||||
236
_scripts/webpack.android.config.js
Normal file
236
_scripts/webpack.android.config.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
||||
const {
|
||||
SHAKA_LOCALE_MAPPINGS,
|
||||
SHAKA_LOCALES_PREBUNDLED,
|
||||
SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
} = require('./getShakaLocales')
|
||||
const { sigViewTemplateParameters } = require('./sigViewConfig')
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
const { version: swiperVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
|
||||
|
||||
const config = {
|
||||
name: 'web',
|
||||
mode: process.env.NODE_ENV,
|
||||
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
||||
entry: {
|
||||
web: path.join(__dirname, '../src/renderer/main.js'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, '../android/app/src/main/assets'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
externals: {
|
||||
android: 'Android'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
whitespace: 'condense',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
implementation: require('sass')
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
esModule: false
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: 'vue-html-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'imgs/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
// webpack defaults to only optimising the production builds, so having this here is fine
|
||||
optimization: {
|
||||
minimizer: [
|
||||
'...', // extend webpack's list instead of overwriting it
|
||||
new JsonMinimizerPlugin({
|
||||
exclude: /\/locales\/.*\.json/
|
||||
}),
|
||||
new CssMinimizerPlugin()
|
||||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: true,
|
||||
__filename: isDevMode,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': false,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.IS_ANDROID': true,
|
||||
'process.env.IS_RELEASE': !isDevMode,
|
||||
'process.env.SUPPORTS_LOCAL_API': true,
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
||||
// video.js' vhs-utils supports both atob() in web browsers and Buffer in node
|
||||
// As the FreeTube web build only runs in web browsers, we can override their check for atob() here: https://github.com/videojs/vhs-utils/blob/main/src/decode-b64-to-uint8-array.js#L3
|
||||
// overriding that check means we don't need to include a Buffer polyfill
|
||||
// https://caniuse.com/atob-btoa
|
||||
|
||||
// NOTE FOR THE FUTURE: this override won't work with vite as their define does a find and replace in the code for production builds,
|
||||
// but uses globals in development builds to save build time, so this would replace the actual atob() function with true if used with vite
|
||||
// this works in webpack as webpack does a find and replace in the source code for both development and production builds
|
||||
// https://vitejs.dev/config/shared-options.html#define
|
||||
// https://webpack.js.org/plugins/define-plugin/
|
||||
'window.atob': true
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser.js'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
excludeChunks: ['processTaskWorker'],
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
nodeModules: false,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "decipher.html",
|
||||
inject: false,
|
||||
templateContent: sigViewTemplateParameters.sigViewRaw,
|
||||
nodeModules: false
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
|
||||
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
|
||||
to: `swiper-${swiperVersion}.css`,
|
||||
context: path.join(__dirname, '../node_modules/swiper/modules'),
|
||||
transformAll: (assets) => {
|
||||
return Buffer.concat(assets.map(asset => asset.data))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: 'vue/dist/vue.runtime.esm.js',
|
||||
'portal-vue$': 'portal-vue/dist/portal-vue.esm.js',
|
||||
|
||||
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/web.js'),
|
||||
|
||||
// change to "shaka-player.ui.debug.js" to get debug logs (update jsconfig to get updated types)
|
||||
'shaka-player$': 'shaka-player/dist/shaka-player.ui.js',
|
||||
'localforage': path.resolve(__dirname, '_localforage.js')
|
||||
},
|
||||
fallback: {
|
||||
'fs/promises': path.resolve(__dirname, '_empty.js'),
|
||||
path: require.resolve('path-browserify'),
|
||||
},
|
||||
extensions: ['.js', '.vue']
|
||||
},
|
||||
target: 'web',
|
||||
}
|
||||
|
||||
const processLocalesPlugin = new ProcessLocalesPlugin({
|
||||
compress: false,
|
||||
inputDir: path.join(__dirname, '../static/locales'),
|
||||
outputDir: 'static/locales',
|
||||
})
|
||||
const processAndroidLocales = new ProcessLocalesPlugin({
|
||||
compress: false,
|
||||
inputDir: path.join(__dirname, '../static/locales-android'),
|
||||
outputDir: 'static/locales-android',
|
||||
})
|
||||
config.plugins.push(
|
||||
processLocalesPlugin,
|
||||
processAndroidLocales,
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
|
||||
'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
||||
'process.env.SHAKA_LOCALE_MAPPINGS': JSON.stringify(SHAKA_LOCALE_MAPPINGS),
|
||||
'process.env.SHAKA_LOCALES_PREBUNDLED': JSON.stringify(SHAKA_LOCALES_PREBUNDLED)
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
|
||||
to: path.join(__dirname, '../android/app/src/main/assets/pwabuilder-sw.js'),
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../static'),
|
||||
to: path.join(__dirname, '../android/app/src/main/assets/static'),
|
||||
globOptions: {
|
||||
dot: true,
|
||||
ignore: ['**/.*', '**/locales/**', '**/locales-android/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
|
||||
},
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'),
|
||||
to: path.join(__dirname, '../android/app/src/main/assets/static/shaka-player-locales'),
|
||||
context: path.join(__dirname, '../node_modules/shaka-player/ui/locales')
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
module.exports = config
|
||||
6
_scripts/webpack.botGuardScript.android.config.js
Normal file
6
_scripts/webpack.botGuardScript.android.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
const config = require('./webpack.botGuardScript.config.js')
|
||||
const { join } = require('path')
|
||||
|
||||
config.output.path = join(__dirname, '../android/app/src/main/assets/')
|
||||
|
||||
module.exports = config
|
||||
23
_scripts/webpack.botGuardScript.config.js
Normal file
23
_scripts/webpack.botGuardScript.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const path = require('path')
|
||||
|
||||
/** @type {import('webpack').Configuration} */
|
||||
module.exports = {
|
||||
name: 'botGuardScript',
|
||||
// Always use production mode, as we use the output as a function body and the debug output doesn't work for that
|
||||
mode: 'production',
|
||||
devtool: false,
|
||||
target: 'web',
|
||||
entry: {
|
||||
botGuardScript: path.join(__dirname, '../src/botGuardScript.js'),
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.join(__dirname, '../dist'),
|
||||
library: {
|
||||
type: 'modern-module'
|
||||
}
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true
|
||||
}
|
||||
}
|
||||
72
_scripts/webpack.main.config.js
Normal file
72
_scripts/webpack.main.config.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
const config = {
|
||||
name: 'main',
|
||||
mode: process.env.NODE_ENV,
|
||||
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
||||
entry: {
|
||||
main: path.join(__dirname, '../src/main/index.js'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
resource: path.resolve(__dirname, '../node_modules/mime-db/db.json'),
|
||||
use: path.join(__dirname, 'mime-db-shrinking-loader.js')
|
||||
}
|
||||
],
|
||||
},
|
||||
// webpack defaults to only optimising the production builds, so having this here is fine
|
||||
optimization: {
|
||||
minimizer: [
|
||||
'...', // extend webpack's list instead of overwriting it
|
||||
new JsonMinimizerPlugin({
|
||||
exclude: /\/locales\/.*\.json/
|
||||
})
|
||||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: isDevMode,
|
||||
__filename: isDevMode
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON_MAIN': true
|
||||
})
|
||||
],
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.join(__dirname, '../dist'),
|
||||
publicPath: ''
|
||||
},
|
||||
target: 'electron-main',
|
||||
}
|
||||
|
||||
if (!isDevMode) {
|
||||
config.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../static'),
|
||||
to: path.join(__dirname, '../dist/static'),
|
||||
globOptions: {
|
||||
dot: true,
|
||||
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/manifest.json', '**/dashFiles/**', '**/storyboards/**'],
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
197
_scripts/webpack.renderer.config.js
Normal file
197
_scripts/webpack.renderer.config.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
const path = require('path')
|
||||
const { readFileSync, readdirSync } = require('fs')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const {
|
||||
SHAKA_LOCALE_MAPPINGS,
|
||||
SHAKA_LOCALES_PREBUNDLED,
|
||||
SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
} = require('./getShakaLocales')
|
||||
const { sigFrameTemplateParameters } = require('./sigFrameConfig')
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
const { version: swiperVersion } = JSON.parse(readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
|
||||
|
||||
const processLocalesPlugin = new ProcessLocalesPlugin({
|
||||
compress: !isDevMode,
|
||||
hotReload: isDevMode,
|
||||
inputDir: path.join(__dirname, '../static/locales'),
|
||||
outputDir: 'static/locales',
|
||||
})
|
||||
|
||||
const config = {
|
||||
name: 'renderer',
|
||||
mode: process.env.NODE_ENV,
|
||||
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
||||
entry: {
|
||||
renderer: path.join(__dirname, '../src/renderer/main.js'),
|
||||
},
|
||||
infrastructureLogging: {
|
||||
// Only warnings and errors
|
||||
// level: 'none' disable logging
|
||||
// Please read https://webpack.js.org/configuration/other-options/#infrastructurelogginglevel
|
||||
level: isDevMode ? 'info' : 'none'
|
||||
},
|
||||
output: {
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.join(__dirname, '../dist'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
whitespace: 'condense',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
implementation: require('sass')
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
esModule: false
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'imgs/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
// webpack defaults to only optimising the production builds, so having this here is fine
|
||||
optimization: {
|
||||
minimizer: [
|
||||
'...', // extend webpack's list instead of overwriting it
|
||||
new CssMinimizerPlugin()
|
||||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
plugins: [
|
||||
processLocalesPlugin,
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': true,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.SUPPORTS_LOCAL_API': true,
|
||||
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
|
||||
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
||||
'process.env.SHAKA_LOCALE_MAPPINGS': JSON.stringify(SHAKA_LOCALE_MAPPINGS),
|
||||
'process.env.SHAKA_LOCALES_PREBUNDLED': JSON.stringify(SHAKA_LOCALES_PREBUNDLED)
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
templateParameters: sigFrameTemplateParameters
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
|
||||
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
|
||||
to: `swiper-${swiperVersion}.css`,
|
||||
context: path.join(__dirname, '../node_modules/swiper/modules'),
|
||||
transformAll: (assets) => {
|
||||
return Buffer.concat(assets.map(asset => asset.data))
|
||||
}
|
||||
},
|
||||
// Don't need to copy them in dev mode,
|
||||
// as we configure WebpackDevServer to serve them
|
||||
...(isDevMode
|
||||
? []
|
||||
: [
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'),
|
||||
to: path.join(__dirname, '../dist/static/shaka-player-locales'),
|
||||
context: path.join(__dirname, '../node_modules/shaka-player/ui/locales'),
|
||||
transform: {
|
||||
transformer: (input) => {
|
||||
return JSON.stringify(JSON.parse(input.toString('utf-8')))
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: 'vue/dist/vue.runtime.esm.js',
|
||||
'portal-vue$': 'portal-vue/dist/portal-vue.esm.js',
|
||||
|
||||
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/electron.js'),
|
||||
|
||||
'youtubei.js$': 'youtubei.js/web',
|
||||
|
||||
// change to "shaka-player.ui.debug.js" to get debug logs (update jsconfig to get updated types)
|
||||
'shaka-player$': 'shaka-player/dist/shaka-player.ui.js',
|
||||
},
|
||||
extensions: ['.js', '.vue']
|
||||
},
|
||||
target: 'electron-renderer',
|
||||
}
|
||||
|
||||
if (isDevMode) {
|
||||
// hack to pass it through to the dev-runner.js script
|
||||
// gets removed there before the config object is passed to webpack
|
||||
config.SHAKA_LOCALES_TO_BE_BUNDLED = SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
220
_scripts/webpack.web.config.js
Normal file
220
_scripts/webpack.web.config.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
||||
const {
|
||||
SHAKA_LOCALE_MAPPINGS,
|
||||
SHAKA_LOCALES_PREBUNDLED,
|
||||
SHAKA_LOCALES_TO_BE_BUNDLED
|
||||
} = require('./getShakaLocales')
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
const { version: swiperVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
|
||||
|
||||
const config = {
|
||||
name: 'web',
|
||||
mode: process.env.NODE_ENV,
|
||||
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
||||
entry: {
|
||||
web: path.join(__dirname, '../src/renderer/main.js'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, '../dist/web'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
externals: {
|
||||
android: '{}',
|
||||
'youtubei.js': '{}'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
whitespace: 'condense',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
implementation: require('sass')
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
esModule: false
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: 'vue-html-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'imgs/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
// webpack defaults to only optimising the production builds, so having this here is fine
|
||||
optimization: {
|
||||
minimizer: [
|
||||
'...', // extend webpack's list instead of overwriting it
|
||||
new JsonMinimizerPlugin({
|
||||
exclude: /\/locales\/.*\.json/
|
||||
}),
|
||||
new CssMinimizerPlugin()
|
||||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': false,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.SUPPORTS_LOCAL_API': false,
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
||||
'process.env.IS_ANDROID': false,
|
||||
'process.env.IS_RELEASE': !isDevMode
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
excludeChunks: ['processTaskWorker'],
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, '../src/index.ejs')
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
|
||||
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
|
||||
to: `swiper-${swiperVersion}.css`,
|
||||
context: path.join(__dirname, '../node_modules/swiper/modules'),
|
||||
transformAll: (assets) => {
|
||||
return Buffer.concat(assets.map(asset => asset.data))
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: 'vue/dist/vue.runtime.esm.js',
|
||||
'portal-vue$': 'portal-vue/dist/portal-vue.esm.js',
|
||||
|
||||
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/web.js'),
|
||||
|
||||
// change to "shaka-player.ui.debug.js" to get debug logs (update jsconfig to get updated types)
|
||||
'shaka-player$': 'shaka-player/dist/shaka-player.ui.js',
|
||||
},
|
||||
fallback: {
|
||||
'fs/promises': path.resolve(__dirname, '_empty.js'),
|
||||
path: require.resolve('path-browserify'),
|
||||
},
|
||||
extensions: ['.js', '.vue']
|
||||
},
|
||||
target: 'web',
|
||||
}
|
||||
|
||||
const processLocalesPlugin = new ProcessLocalesPlugin({
|
||||
compress: false,
|
||||
hotReload: isDevMode,
|
||||
inputDir: path.join(__dirname, '../static/locales'),
|
||||
outputDir: 'static/locales',
|
||||
})
|
||||
|
||||
config.plugins.push(
|
||||
processLocalesPlugin,
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
|
||||
'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
||||
'process.env.SHAKA_LOCALE_MAPPINGS': JSON.stringify(SHAKA_LOCALE_MAPPINGS),
|
||||
'process.env.SHAKA_LOCALES_PREBUNDLED': JSON.stringify(SHAKA_LOCALES_PREBUNDLED)
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../_icons/192x192.png'),
|
||||
to: path.join(__dirname, '../dist/web/static/_icons/192x192.png'),
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../_icons/512x512.png'),
|
||||
to: path.join(__dirname, '../dist/web/static/_icons/512x512.png'),
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
|
||||
to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'),
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../static'),
|
||||
to: path.join(__dirname, '../dist/web/static'),
|
||||
globOptions: {
|
||||
dot: true,
|
||||
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
|
||||
},
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'),
|
||||
to: path.join(__dirname, '../dist/web/static/shaka-player-locales'),
|
||||
context: path.join(__dirname, '../node_modules/shaka-player/ui/locales')
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
module.exports = config
|
||||
Loading…
Add table
Add a link
Reference in a new issue