diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d0f21007f5..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,67 +0,0 @@ -.DS_Store -.thumbs.db -node_modules - -# Quasar core related directories -.quasar -/dist -/quasar.config.*.temporary.compiled* - -# Cordova related directories and files -/src-cordova - -# Capacitor related directories and files -/src-capacitor - -# BEX related directories and files -/src-bex/www -/src-bex/js/core - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -*.suo -*.ntvs* -*.njsproj -*.sln - -# local .env files -.env* -!.env.example - -# Sentry Config File -.env.sentry-build-plugin - -# Yarn files -.pnp.* -.yarn/* - -# VitePress -docs/.vitepress/dist -docs/.vitepress/cache - -# Output of 'yarn icons' -src/css/mmm-icons.* - -# Custom dep helper script -depChecker.js - -# Legacy files -build/win*-unpacked -build/*.yml -build/*.blockmap -build/*.exe -src/main -src/renderer - -# Translated Markdown files -docs/src/**/* -!docs/src/en/**/* - -# Generated files -LICENCE.md -.eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index c35cf3ef53..0000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,108 +0,0 @@ -module.exports = { - // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy - // This option interrupts the configuration hierarchy at this file - // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) - root: true, - - // https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser - // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working - // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted - parserOptions: { - parser: require.resolve('@typescript-eslint/parser'), - extraFileExtensions: ['.vue'], - }, - - env: { - browser: true, - es2021: true, - node: true, - }, - - // Rules order is important, please avoid shuffling them - extends: [ - // Base ESLint recommended rules - 'eslint:recommended', - - // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage - // ESLint typescript rules - 'plugin:@typescript-eslint/strict', - 'plugin:@typescript-eslint/stylistic', - - 'plugin:perfectionist/recommended-natural-legacy', - - // See https://eslint.vuejs.org/rules/#available-rules - 'plugin:vue/vue3-recommended', - - // https://github.com/prettier/eslint-config-prettier#installation - // usage with Prettier, provided by 'eslint-config-prettier'. - 'prettier', - ], - - plugins: [ - // required to apply rules which need type information - '@typescript-eslint', - - // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files - // required to lint *.vue files - 'vue', - ], - - globals: { - ga: 'readonly', // Google Analytics - cordova: 'readonly', - __statics: 'readonly', - __QUASAR_SSR__: 'readonly', - __QUASAR_SSR_SERVER__: 'readonly', - __QUASAR_SSR_CLIENT__: 'readonly', - __QUASAR_SSR_PWA__: 'readonly', - process: 'readonly', - Capacitor: 'readonly', - chrome: 'readonly', - }, - - // add your custom rules here - rules: { - // eslint rules - 'array-callback-return': 'error', - 'no-constant-binary-expression': 'error', - 'no-constructor-return': 'error', - 'no-template-curly-in-string': 'error', - 'no-unreachable-loop': 'error', - 'no-unmodified-loop-condition': 'error', - 'no-duplicate-imports': 'error', - 'no-self-compare': 'error', - 'no-new-native-nonconstructor': 'error', - 'no-promise-executor-return': 'error', - 'prefer-promise-reject-errors': 'off', - - // vue rules - 'vue/block-lang': ['error', { script: { lang: 'ts' } }], - 'vue/block-order': 'error', - 'vue/component-api-style': ['error', ['script-setup']], - 'vue/define-props-declaration': 'error', - 'vue/enforce-style-attribute': 'error', - 'vue/no-boolean-default': 'error', - 'vue/attributes-order': ['error', { alphabetical: true }], - 'vue/prefer-true-attribute-shorthand': 'error', - - // typescript-eslint rules - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/no-import-type-side-effects': 'error', - - quotes: ['warn', 'single', { avoidEscape: true }], - - // this rule, if on, would require explicit return type on the `render` function - '@typescript-eslint/explicit-function-return-type': 'off', - - // in plain CommonJS modules, you can't use `import foo = require('foo')` to pass this rule, so it has to be disabled - '@typescript-eslint/no-var-requires': 'off', - - // The core 'no-unused-vars' rules (in the eslint:recommended ruleset) - // does not work with type definitions - 'no-undef': 'off', - 'no-unused-vars': 'off', - - // allow debugger during development only - 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', - }, -}; diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a2f3bfa5d0..493980996c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,10 +22,6 @@ updates: ignore: - dependency-name: '@types/node' update-types: ['version-update:semver-major'] - - dependency-name: '@typescript-eslint/*' - update-types: ['version-update:semver-major'] - - dependency-name: 'eslint' - update-types: ['version-update:semver-major'] - dependency-name: '@sentry/vue' update-types: ['version-update:semver-major', 'version-update:semver-minor'] @@ -33,10 +29,6 @@ updates: update-types: ['version-update:semver-major', 'version-update:semver-minor'] groups: - typescript-eslint: - applies-to: version-updates - patterns: - - '@typescript-eslint/*' vueuse: applies-to: version-updates patterns: diff --git a/.yarnrc.yml b/.yarnrc.yml index 32486f648d..32670d1e5b 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,4 +1,4 @@ -afterInstall: (node .husky/install.mjs && yarn electron-rebuild && yarn icons) || exit 0 +afterInstall: (yarn quasar prepare && node .husky/install.mjs && yarn electron-rebuild && yarn icons) || exit 0 nodeLinker: node-modules diff --git a/README.md b/README.md index b09e8a6702..421a37f827 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,6 @@ In addition, M³ itself has been translated into several languages by many volun - #### Available
Chinese Simplified
100%
Dutch
100%
French
100%
German
100%
Italian
100%
Portuguese
100%
Portuguese, Brazilian
100%
Slovenian
100%
Swedish
100%
Ukrainian
100%
Estonian
99%
Swahili
99%
Russian
95%
Hungarian
57%
Spanish
54%
diff --git a/build/svg2font.js b/build/svg2font.js index f62672b7ba..aade7e1038 100644 --- a/build/svg2font.js +++ b/build/svg2font.js @@ -1,5 +1,4 @@ -// eslint-disable-next-line @typescript-eslint/no-require-imports -var IconfontWebpackPlugin = require('@daipeng7/rollup-plugin-iconfont'); +import IconfontWebpackPlugin from '@daipeng7/rollup-plugin-iconfont'; var options = { cssOutput: 'src/css/mmm-icons.css', // iconFont name prefix diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 1f4c502ad4..35a169f5be 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,9 +1,11 @@ import { defineConfig } from 'vitepress'; -import messages, { localeOptions, enabled } from './../locales'; -import { mapLocales, mapSearch } from './../utils/locales'; + +import type { LanguageValue } from '../../src/constants/locales'; + +import messages, { enabled, localeOptions } from './../locales'; import { CANONICAL_URL, GH_REPO, GH_REPO_URL } from './../utils/constants'; import { camelToKebabCase, kebabToCamelCase } from './../utils/general'; -import type { LanguageValue } from '../../src/constants/locales'; +import { mapLocales, mapSearch } from './../utils/locales'; const base = `/${GH_REPO}/`; const srcExclude = localeOptions @@ -13,68 +15,63 @@ const srcExclude = localeOptions // https://vitepress.dev/reference/site-config export default defineConfig({ base, - srcDir: './src', cleanUrls: true, - lastUpdated: true, - rewrites: { 'en/:rest*': ':rest*' }, - markdown: { image: { lazyLoading: true } }, - srcExclude, head: [ [ 'link', { + href: `${base}icons/favicon-128x128.png`, rel: 'icon', - type: 'image/png', sizes: '128x128', - href: `${base}icons/favicon-128x128.png`, + type: 'image/png', }, ], [ 'link', { + href: `${base}icons/favicon-96x96.png`, rel: 'icon', - type: 'image/png', sizes: '96x96', - href: `${base}icons/favicon-96x96.png`, + type: 'image/png', }, ], [ 'link', { + href: `${base}icons/favicon-32x32.png`, rel: 'icon', - type: 'image/png', sizes: '32x32', - href: `${base}icons/favicon-32x32.png`, + type: 'image/png', }, ], [ 'link', { + href: `${base}icons/favicon-16x16.png`, rel: 'icon', - type: 'image/png', sizes: '16x16', - href: `${base}icons/favicon-16x16.png`, + type: 'image/png', }, ], - ['link', { rel: 'icon', type: 'image/ico', href: `${base}favicon.ico` }], + ['link', { href: `${base}favicon.ico`, rel: 'icon', type: 'image/ico' }], [ 'link', { + href: `${base}logo.svg`, rel: 'icon', type: 'image/svg+xml', - href: `${base}logo.svg`, }, ], - ['meta', { name: 'color-scheme', content: 'light dark' }], - ['meta', { name: 'theme-color', content: '#3075F2' }], - ['meta', { name: 'mobile-web-app-capable', content: 'yes' }], - ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], + ['meta', { content: 'light dark', name: 'color-scheme' }], + ['meta', { content: '#3075F2', name: 'theme-color' }], + ['meta', { content: 'yes', name: 'mobile-web-app-capable' }], + ['meta', { content: 'yes', name: 'apple-mobile-web-app-capable' }], [ 'meta', - { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }, + { content: 'black', name: 'apple-mobile-web-app-status-bar-style' }, ], - ['meta', { name: 'twitter:card', content: 'summary_large_image' }], - ['meta', { name: 'twitter:image:alt', content: 'M³ project cover' }], + ['meta', { content: 'summary_large_image', name: 'twitter:card' }], + ['meta', { content: 'M³ project cover', name: 'twitter:image:alt' }], [ 'meta', { content: `${CANONICAL_URL}m3-project-cover.png`, property: 'og:image' }, @@ -83,7 +80,7 @@ export default defineConfig({ ['meta', { content: '1442', property: 'og:image:width' }], ['meta', { content: '865', property: 'og:image:height' }], ['meta', { content: 'M³ project cover', property: 'og:image:alt' }], - ['meta', { property: 'og:type', content: 'website' }], + ['meta', { content: 'website', property: 'og:type' }], ['meta', { content: `${CANONICAL_URL}icon.png`, property: 'og:image' }], ['meta', { content: 'image/png', property: 'og:image:type' }], ['meta', { content: '512', property: 'og:image:width' }], @@ -98,6 +95,18 @@ export default defineConfig({ ['meta', { content: '640', property: 'og:image:height' }], ['meta', { content: 'M³ repo preview banner', property: 'og:image:alt' }], ], + lastUpdated: true, + locales: mapLocales(), + markdown: { image: { lazyLoading: true } }, + rewrites: { 'en/:rest*': ':rest*' }, + srcDir: './src', + srcExclude, + themeConfig: { + externalLinkIcon: true, + logo: '/logo.svg', + search: mapSearch(), + socialLinks: [{ ariaLabel: 'GitHub', icon: 'github', link: GH_REPO_URL }], + }, transformPageData(pageData) { const canonicalUrl = `${CANONICAL_URL}${pageData.relativePath}` .replace(/index\.md$/, '') @@ -109,28 +118,28 @@ export default defineConfig({ pageData.frontmatter.head ??= []; pageData.frontmatter.head.push( - ['link', { rel: 'canonical', href: canonicalUrl }], - ['meta', { property: 'og:url', content: canonicalUrl }], + ['link', { href: canonicalUrl, rel: 'canonical' }], + ['meta', { content: canonicalUrl, property: 'og:url' }], [ 'meta', { - property: 'og:title', content: pageData.frontmatter.layout === 'home' ? isEnglish ? messages['en'].title : messages[messageLocale].title : `${pageData.title} | M³ docs`, + property: 'og:title', }, ], [ 'link', { - rel: 'alternate', - hreflang: 'x-default', href: isEnglish ? canonicalUrl : canonicalUrl.replace(`/${pageLang}/`, '/'), + hreflang: 'x-default', + rel: 'alternate', }, ], ...localeOptions @@ -140,12 +149,12 @@ export default defineConfig({ return [ 'link', { - rel: 'alternate', - hreflang: lang, href: (!isEnglish ? canonicalUrl.replace(`/${pageLang}/`, `/${lang}/`) : `${CANONICAL_URL}${lang}/${pageData.relativePath.replace('index.md', '').replace('.md', '')}` ).replace('/en/', '/'), + hreflang: lang, + rel: 'alternate', }, ]; }), @@ -188,11 +197,4 @@ export default defineConfig({ ], ); }, - locales: mapLocales(), - themeConfig: { - externalLinkIcon: true, - logo: '/logo.svg', - search: mapSearch(), - socialLinks: [{ icon: 'github', link: GH_REPO_URL, ariaLabel: 'GitHub' }], - }, }); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index fccaf72809..0696f84af9 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,17 +1,20 @@ // https://vitepress.dev/guide/custom-theme -import { h } from 'vue'; import type { Theme } from 'vitepress'; + import DefaultTheme from 'vitepress/theme'; +import { h } from 'vue'; + import './style.css'; export default { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + enhanceApp({ app, router, siteData }) { + // ... + }, extends: DefaultTheme, Layout: () => { return h(DefaultTheme.Layout, null, { // https://vitepress.dev/guide/extending-default-theme#layout-slots }); }, - enhanceApp({ app, router, siteData }) { - // ... - }, } satisfies Theme; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000..ef33ede314 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,103 @@ +import js from '@eslint/js'; +import pluginQuasar from '@quasar/app-vite/eslint'; +import skipFormattingConfig from '@vue/eslint-config-prettier/skip-formatting'; +import vueTsEslintConfig from '@vue/eslint-config-typescript'; +import perfectionist from 'eslint-plugin-perfectionist'; +import pluginVue from 'eslint-plugin-vue'; +import globals from 'globals'; + +export default [ + { + ignores: [ + '**/.DS_Store', + '.yarn/', + 'src/css/mmm-icons.css', + 'build/', + '!build/svg2font.js', + 'src/renderer/', + 'src/main/', + 'docs/src/**/*', + '!docs/src/en/**/*', + 'LICENCE.md', + 'docs/.vitepress/dist', + 'docs/.vitepress/cache', + ], + }, + + ...pluginQuasar.configs.recommended(), + js.configs.recommended, + + // https://eslint.vuejs.org + ...pluginVue.configs['flat/recommended'], + + // https://typescript-eslint.io/rules/ + { + files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.vue'], + rules: { + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + }, + }, + + // https://github.com/vuejs/eslint-config-typescript + ...vueTsEslintConfig({ + // https://typescript-eslint.io/users/configs#recommended-configurations + extends: ['strict', 'stylistic'], + }), + + // https://typescript-eslint.io/rules/ + { + files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.vue'], + rules: { + // Turn off the recommended rules that you don't need. + }, + }, + + perfectionist.configs['recommended-natural'], + + { + languageOptions: { + ecmaVersion: 'latest', + globals: { + ...globals.browser, + ...globals.node, // SSR, Electron, config files + browser: 'readonly', // bex related + Capacitor: 'readonly', + chrome: 'readonly', // bex related + cordova: 'readonly', + ga: 'readonly', // Google Analytics + process: 'readonly', // process.env.* + }, + + sourceType: 'module', + }, + + rules: { + // https://eslint.org/docs/latest/rules/ + 'array-callback-return': 'error', + 'no-constant-binary-expression': 'error', + 'no-constructor-return': 'error', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'no-duplicate-imports': 'error', + 'no-new-native-nonconstructor': 'error', + 'no-promise-executor-return': 'error', + 'no-self-compare': 'error', + 'no-template-curly-in-string': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unreachable-loop': 'error', + 'prefer-promise-reject-errors': 'off', + + // https://eslint.vuejs.org/rules/ + 'vue/attributes-order': ['error', { alphabetical: true }], + 'vue/block-lang': ['error', { script: { lang: 'ts' } }], + 'vue/block-order': 'error', + 'vue/component-api-style': ['error', ['script-setup']], + 'vue/define-props-declaration': 'error', + 'vue/enforce-style-attribute': 'error', + 'vue/no-boolean-default': 'error', + 'vue/prefer-true-attribute-shorthand': 'error', + }, + }, + + skipFormattingConfig, +]; diff --git a/package.json b/package.json index a5bff75ec3..3b6b35a9ae 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "format": "prettier \"**/*\" --ignore-unknown --write", - "lint": "eslint --ext .js,.ts,.mjs,.mts,.vue ./", - "type-check": "vue-tsc --noEmit -p tsconfig.vue-tsc.json", + "lint": "eslint -c ./eslint.config.js './**/*.{js,ts,cjs,mjs,mts,vue}'", + "type-check": "vue-tsc --noEmit -p tsconfig.json", "test": "echo \"No test specified\" && exit 0" }, "dependencies": { @@ -69,8 +69,8 @@ "devDependencies": { "@daipeng7/rollup-plugin-iconfont": "^3.0.6", "@electron/rebuild": "^3.7.1", - "@intlify/vite-plugin-vue-i18n": "^7.0.0", - "@quasar/app-vite": "^1.11.0", + "@intlify/unplugin-vue-i18n": "^6.0.0", + "@quasar/app-vite": "^2.0.0", "@rollup/plugin-inject": "^5.0.5", "@sentry/esbuild-plugin": "^2.22.7", "@sentry/vite-plugin": "^2.22.7", @@ -82,15 +82,15 @@ "@types/node": "^20.17.8", "@types/pretty-bytes": "^5.2.0", "@types/sanitize-html": "^2.13.0", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@vue/eslint-config-prettier": "^10.1.0", + "@vue/eslint-config-typescript": "^14.1.3", "autoprefixer": "^10.4.20", "electron": "^33.2.0", "electron-builder": "^25.1.8", - "eslint": "^8.57.1", - "eslint-config-prettier": "^9.1.0", + "eslint": "^9.16.0", "eslint-plugin-perfectionist": "^4.2.0", "eslint-plugin-vue": "^9.32.0", + "globals": "^15.13.0", "husky": "^9.1.7", "lint-staged": "^15.2.10", "postcss": "^8.4.49", @@ -104,7 +104,7 @@ ], "lint-staged": { "*": "prettier --write --ignore-unknown", - "*.{js,ts,mjs,mts,vue}": "eslint --fix" + "*.{js,ts,cjs,mjs,mts,vue}": "eslint --fix" }, "engines": { "node": "^20.18.0", @@ -116,6 +116,7 @@ "win32" ], "private": true, + "type": "module", "packageManager": "yarn@4.5.3", "productName": "Meeting Media Manager" } diff --git a/postcss.config.cjs b/postcss.config.cjs deleted file mode 100644 index a813c23a25..0000000000 --- a/postcss.config.cjs +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ -// https://github.com/michael-ciniawsky/postcss-load-config - -module.exports = { - plugins: [ - // https://github.com/postcss/autoprefixer - require('autoprefixer')({ overrideBrowserslist: ['Electron >= 33'] }), - - // https://github.com/elchininet/postcss-rtlcss - // If you want to support RTL css, then - // 1. yarn/npm install postcss-rtlcss - // 2. optionally set quasar.config.js > framework > lang to an RTL language - // 3. uncomment the following line: - // require('postcss-rtlcss') - ], -}; diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..06f9c4d71d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,18 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +import autoprefixer from 'autoprefixer'; +// import rtlcss from 'postcss-rtlcss' + +export default { + plugins: [ + // https://github.com/postcss/autoprefixer + autoprefixer({ overrideBrowserslist: ['Electron >= 33'] }), + + // https://github.com/elchininet/postcss-rtlcss + // If you want to support RTL css, then + // 1. yarn/pnpm/bun/npm install postcss-rtlcss + // 2. optionally set quasar.config.ts > framework > lang to an RTL language + // 3. uncomment the following line (and its import statement above): + // rtlcss() + ], +}; diff --git a/quasar.config.js b/quasar.config.ts similarity index 57% rename from quasar.config.js rename to quasar.config.ts index 879295b564..c88c035578 100644 --- a/quasar.config.js +++ b/quasar.config.ts @@ -1,90 +1,86 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -/* eslint-env node */ - -/* - * This file runs in a Node context (it's NOT transpiled by Babel), so use only - * the ES6 features that are supported by your Node version. https://node.green/ - */ - // Configuration for your app -// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js -const { sentryEsbuildPlugin } = require('@sentry/esbuild-plugin'); -const { sentryVitePlugin } = require('@sentry/vite-plugin'); -const path = require('path'); -const { configure } = require('quasar/wrappers'); -const { mergeConfig } = require('vite'); // use mergeConfig helper to avoid overwriting the default config +// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file -const { repository, version } = require('./package.json'); +import { defineConfig } from '#q-app/wrappers'; +import { sentryEsbuildPlugin } from '@sentry/esbuild-plugin'; +import { sentryVitePlugin } from '@sentry/vite-plugin'; +import { fileURLToPath } from 'node:url'; +import { mergeConfig } from 'vite'; // use mergeConfig helper to avoid overwriting the default config + +import { repository, version } from './package.json'; const SENTRY_ORG = 'jw-projects'; const SENTRY_PROJECT = 'mmm-v2'; const SENTRY_VERSION = `meeting-media-manager@${version}`; -module.exports = configure(function (ctx) { +export default defineConfig((ctx) => { return { - // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature - // preFetch: true, - + // animations: 'all', // --- includes all animations // https://v2.quasar.dev/options/animations animations: ['fadeIn', 'fadeOut'], + // app boot file (/src/boot) + // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli-vite/boot-files boot: ['sentry', 'i18n', 'globals'], - // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build + // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build build: { env: { repository: repository.url.replace('.git', ''), }, extendViteConf(viteConf) { - viteConf.optimizeDeps = mergeConfig(viteConf, { - esbuildOptions: { define: { global: 'window' } }, + viteConf.optimizeDeps = mergeConfig(viteConf.optimizeDeps ?? {}, { + esbuildOptions: { + define: { + global: 'window', + }, + }, }); - if (ctx.prod && !ctx.debug && process.env.SENTRY_AUTH_TOKEN) { - if (!viteConf.build) viteConf.build = {}; - viteConf.build.sourcemap = true; + if (ctx.prod && !ctx.debug) { + viteConf.build = mergeConfig(viteConf.build ?? {}, { + sourcemap: true, + }); if (!viteConf.plugins) viteConf.plugins = []; - viteConf.plugins.push( - sentryVitePlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN, - org: SENTRY_ORG, - project: SENTRY_PROJECT, - release: { name: SENTRY_VERSION }, - telemetry: false, - }), - ); + if (process.env.SENTRY_AUTH_TOKEN) { + viteConf.plugins.push( + sentryVitePlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: SENTRY_ORG, + project: SENTRY_PROJECT, + release: { name: SENTRY_VERSION }, + telemetry: false, + }), + ); + } } }, sourcemap: true, // See: https://www.electronjs.org/docs/latest/tutorial/electron-timelines#timeline target: { browser: ['chrome130'], node: 'node20.18.0' }, + typescript: { + extendTsConfig: (tsConfig) => { + tsConfig.exclude?.push('./../docs'); + }, + strict: true, + vueShim: true, + }, vitePlugins: [ [ - '@intlify/vite-plugin-vue-i18n', + '@intlify/unplugin-vue-i18n/vite', { - include: path.resolve(__dirname, './src/i18n/**'), + include: [fileURLToPath(new URL('./src/i18n', import.meta.url))], + ssr: ctx.modeName === 'ssr', }, ], ], vueRouterMode: 'hash', // available values: 'hash', 'history' }, - // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css + // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css css: ['app.scss', 'mmm-icons.css'], - // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles - // sourceFiles: { - // rootComponent: 'src/App.vue', - // router: 'src/router/index', - // store: 'src/store/index', - // registerServiceWorker: 'src-pwa/register-service-worker', - // serviceWorker: 'src-pwa/custom-service-worker', - // pwaManifestFile: 'src-pwa/manifest.json', - // electronMain: 'src-electron/electron-main', - // electronPreload: 'src-electron/electron-preload' - // }, - - // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer + // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devServer devServer: { // https: true open: true, // opens browser window automatically @@ -127,7 +123,7 @@ module.exports = configure(function (ctx) { icon: 'icons/icon.ico', publish: ['github'], target: [ - { arch: ctx.debug ? undefined : ['x64', 'ia32'], target: 'nsis' }, + { arch: ctx.debug ? 'x64' : ['x64', 'ia32'], target: 'nsis' }, 'portable', ], }, @@ -137,30 +133,34 @@ module.exports = configure(function (ctx) { if (ctx.prod && !ctx.debug && process.env.SENTRY_AUTH_TOKEN) { esbuildConf.sourcemap = true; if (!esbuildConf.plugins) esbuildConf.plugins = []; - esbuildConf.plugins.push( - sentryEsbuildPlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN, - org: SENTRY_ORG, - project: SENTRY_PROJECT, - release: { name: SENTRY_VERSION }, - telemetry: false, - }), - ); + if (process.env.SENTRY_AUTH_TOKEN) { + esbuildConf.plugins.push( + sentryEsbuildPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: SENTRY_ORG, + project: SENTRY_PROJECT, + release: { name: SENTRY_VERSION }, + telemetry: false, + }), + ); + } } }, extendElectronPreloadConf: (esbuildConf) => { if (ctx.prod && !ctx.debug && process.env.SENTRY_AUTH_TOKEN) { esbuildConf.sourcemap = true; if (!esbuildConf.plugins) esbuildConf.plugins = []; - esbuildConf.plugins.push( - sentryEsbuildPlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN, - org: SENTRY_ORG, - project: SENTRY_PROJECT, - release: { name: SENTRY_VERSION }, - telemetry: false, - }), - ); + if (process.env.SENTRY_AUTH_TOKEN) { + esbuildConf.plugins.push( + sentryEsbuildPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: SENTRY_ORG, + project: SENTRY_PROJECT, + release: { name: SENTRY_VERSION }, + telemetry: false, + }), + ); + } } }, extendPackageJson(pkg) { @@ -191,26 +191,11 @@ module.exports = configure(function (ctx) { } }); }, - inspectPort: 5858, }, - // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework + // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework framework: { - config: { - dark: 'auto', - }, - - // iconSet: 'material-icons', // Quasar icon set - // lang: 'en-US', // Quasar language pack - - // For special cases outside of where the auto-import strategy can have an impact - // (like functional components as one of the examples), - // you can manually specify Quasar components/directives to be available everywhere: - // - // components: [], - // directives: [], - - // Quasar plugins + config: { dark: 'auto' }, plugins: ['LocalStorage', 'Notify'], }, }; diff --git a/src-electron/constants.ts b/src-electron/constants.ts index ed82294dba..2e80c65ce9 100644 --- a/src-electron/constants.ts +++ b/src-electron/constants.ts @@ -10,4 +10,4 @@ export const TRUSTED_DOMAINS: string[] = [ 'akamaihd.net', 'cloudfront.net', ]; -export const HD_RESOLUTION = [1280, 720]; +export const HD_RESOLUTION = [1280, 720] as const; diff --git a/src-electron/electron-env.d.ts b/src-electron/electron-env.d.ts index 359c39de9c..bb6c8e92e7 100644 --- a/src-electron/electron-env.d.ts +++ b/src-electron/electron-env.d.ts @@ -3,7 +3,8 @@ declare namespace NodeJS { interface ProcessEnv { QUASAR_PUBLIC_FOLDER: string; - QUASAR_ELECTRON_PRELOAD: string; + QUASAR_ELECTRON_PRELOAD_FOLDER: string; + QUASAR_ELECTRON_PRELOAD_EXTENSION: string; APP_URL: string; } } diff --git a/src-electron/electron-flag.d.ts b/src-electron/electron-flag.d.ts deleted file mode 100644 index ffbda77ecf..0000000000 --- a/src-electron/electron-flag.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -// THIS FEATURE-FLAG FILE IS AUTOGENERATED, -// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING -import 'quasar/dist/types/feature-flag'; - -declare module 'quasar/dist/types/feature-flag' { - interface QuasarFeatureFlags { - electron: true; - } -} diff --git a/src-electron/electron-main.ts b/src-electron/electron-main.ts index 6a0dfc1a82..ebd3a9fe86 100644 --- a/src-electron/electron-main.ts +++ b/src-electron/electron-main.ts @@ -12,7 +12,8 @@ import { shell, } from 'electron'; import { SENTRY_DSN } from 'src/constants/sentry'; -import { join } from 'upath'; +import upath from 'upath'; +const { join } = upath; import { PLATFORM } from './constants'; import { cancelAllDownloads } from './main/downloads'; diff --git a/src-electron/main/downloads.ts b/src-electron/main/downloads.ts index 1d779cd0cd..2c8678b62d 100644 --- a/src-electron/main/downloads.ts +++ b/src-electron/main/downloads.ts @@ -1,7 +1,8 @@ import { getCountriesForTimezone as _0x2d6c } from 'countries-and-timezones'; import { ElectronDownloadManager } from 'electron-dl-manager'; -import { ensureDir } from 'fs-extra'; -import { basename } from 'upath'; +import { ensureDir } from 'fs-extra/esm'; +import upath from 'upath'; +const { basename } = upath; import { captureElectronError, fetchJson } from './utils'; import { sendToWindow } from './window/window-base'; @@ -116,7 +117,7 @@ export async function isDownloadErrorExpected() { String.fromCharCode(0x74, 0x69, 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65) ]; const _0x66b7 = _0x2d6c(_0x8d1b); - if (_0x66b7.length === 1) _0x5f0a = _0x66b7[0].id; + if (_0x66b7.length === 1) _0x5f0a = _0x66b7[0]?.id ?? ''; } if (!_0x5f0a) { diff --git a/src-electron/main/fs.ts b/src-electron/main/fs.ts index e0fa0230ee..a86869887e 100644 --- a/src-electron/main/fs.ts +++ b/src-electron/main/fs.ts @@ -2,13 +2,15 @@ import type { FileDialogFilter, FileItem } from 'src/types'; import { watch as filesystemWatch, type FSWatcher } from 'chokidar'; import { dialog } from 'electron'; -import { type Dirent, exists, readdir, stat, type Stats } from 'fs-extra'; +import fse, { type Dirent, type Stats } from 'fs-extra'; +const { exists, readdir, stat } = fse; import { IMG_EXTENSIONS, JWPUB_EXTENSIONS, PDF_EXTENSIONS, } from 'src/constants/media'; -import { basename, dirname, join, toUnix } from 'upath'; +import upath from 'upath'; +const { basename, dirname, join, toUnix } = upath; import { captureElectronError } from './utils'; import { sendToWindow } from './window/window-base'; diff --git a/src-electron/main/updater.ts b/src-electron/main/updater.ts index a1eb336dcc..3ff59f92d9 100644 --- a/src-electron/main/updater.ts +++ b/src-electron/main/updater.ts @@ -1,6 +1,8 @@ import { app } from 'electron'; -import { autoUpdater } from 'electron-updater'; -import { exists } from 'fs-extra'; +import electronUpdater from 'electron-updater'; +const { autoUpdater } = electronUpdater; +import fse from 'fs-extra'; +const { exists } = fse; import { join } from 'path'; import { PLATFORM } from './../constants'; diff --git a/src-electron/main/utils.ts b/src-electron/main/utils.ts index 48f5372293..042f5abe8f 100644 --- a/src-electron/main/utils.ts +++ b/src-electron/main/utils.ts @@ -1,4 +1,4 @@ -import type { ExclusiveEventHintOrCaptureContext } from '@sentry/core/build/types/utils/prepareEvent'; +import type { ExclusiveEventHintOrCaptureContext } from 'app/node_modules/@sentry/core/build/types/utils/prepareEvent'; import { captureException } from '@sentry/electron/main'; import { version } from 'app/package.json'; @@ -29,7 +29,7 @@ export function isJwDomain(url: string): boolean { .filter((d): d is string => !!d) .map((d) => new URL(`https://${d}/`).hostname), ).some((domain) => parsedUrl.hostname.endsWith(domain)); - } catch (e) { + } catch { return false; } } @@ -51,7 +51,7 @@ export function isSelf(url?: string): boolean { parsedUrl.protocol === 'file:' && parsedUrl.pathname === parsedAppUrl.pathname) ); - } catch (e) { + } catch { return false; } } @@ -61,7 +61,8 @@ export function isSelf(url?: string): boolean { * @param url The url to check * @returns Wether the url is a trusted domain */ -export function isTrustedDomain(url: string): boolean { +export function isTrustedDomain(url?: string): boolean { + if (!url) return false; try { const parsedUrl = new URL(url); if (parsedUrl.protocol !== 'https:') return false; @@ -74,7 +75,7 @@ export function isTrustedDomain(url: string): boolean { .filter((d): d is string => !!d) .map((d) => new URL(d).hostname), ).some((domain) => parsedUrl.hostname.endsWith(domain)); - } catch (e) { + } catch { return false; } } @@ -88,7 +89,7 @@ export const isValidUrl = (url: string): boolean => { try { new URL(url); return true; - } catch (e) { + } catch { return false; } }; @@ -185,7 +186,6 @@ export function captureElectronError( */ export const throttle = (func: (...args: T[]) => void, delay: number) => { let prev = 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any return (...args: T[]) => { const now = new Date().getTime(); if (now - prev > delay) { diff --git a/src-electron/main/window/window-base.ts b/src-electron/main/window/window-base.ts index 8768e39491..04ec67873b 100644 --- a/src-electron/main/window/window-base.ts +++ b/src-electron/main/window/window-base.ts @@ -7,6 +7,7 @@ import { type BrowserWindowConstructorOptions, } from 'electron'; import { join, resolve } from 'path'; +import { fileURLToPath } from 'url'; import { urlVariables } from './../session'; import { captureElectronError } from './../utils'; @@ -44,7 +45,7 @@ export function createWindow( height: defaultSize.height, icon: resolve( join( - __dirname, + fileURLToPath(new URL('.', import.meta.url)), 'icons', `icon.${PLATFORM === 'win32' ? 'ico' : PLATFORM === 'darwin' ? 'icns' : 'png'}`, ), @@ -57,10 +58,18 @@ export function createWindow( ...(options ?? {}), webPreferences: { backgroundThrottling: false, + contextIsolation: true, preload: name === 'website' ? undefined - : resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD), + : resolve( + fileURLToPath(new URL('.', import.meta.url)), + join( + process.env.QUASAR_ELECTRON_PRELOAD_FOLDER, + 'electron-preload' + + process.env.QUASAR_ELECTRON_PRELOAD_EXTENSION, + ), + ), sandbox: name === 'website', webSecurity: !IS_DEV, ...(options?.webPreferences ?? {}), @@ -95,9 +104,13 @@ export function createWindow( page = `https://www.${urlVariables?.base || 'jw.org'}/${lang}`; break; } - win.loadURL( - page.startsWith('https') ? page : process.env.APP_URL + `?page=${page}`, //+ `#${page}`, - ); + if (page.startsWith('https://')) { + win.loadURL(page); + } else if (process.env.DEV) { + win.loadURL(process.env.APP_URL + `?page=${page}`); + } else { + win.loadFile('index.html', { query: { page } }); + } // Devtools let devToolsOpenedCount = 0; // Track the number of times the devtools-opened event is fired diff --git a/src-electron/main/window/window-media.ts b/src-electron/main/window/window-media.ts index 8115e8c63b..e6a5c861dc 100644 --- a/src-electron/main/window/window-media.ts +++ b/src-electron/main/window/window-media.ts @@ -3,6 +3,7 @@ import type { ScreenPreferences } from 'src/types'; import { HD_RESOLUTION, PLATFORM } from 'app/src-electron/constants'; import { join, resolve } from 'path'; +import { fileURLToPath } from 'url'; import { getAllScreens, getWindowScreen, screenPreferences } from './../screen'; import { captureElectronError } from './../utils'; @@ -28,7 +29,7 @@ export function createMediaWindow() { height: HD_RESOLUTION[1], icon: resolve( join( - __dirname, + fileURLToPath(new URL('.', import.meta.url)), 'icons', `media-player.${PLATFORM === 'win32' ? 'ico' : PLATFORM === 'darwin' ? 'icns' : 'png'}`, ), @@ -48,7 +49,7 @@ export function createMediaWindow() { }); } -const mediaWindowIsFullScreen = (parentScreenBounds: Electron.Rectangle) => +const mediaWindowIsFullScreen = (parentScreenBounds?: Electron.Rectangle) => boundsAreSame(mediaWindow?.getBounds(), parentScreenBounds) || mediaWindow?.isFullScreen(); @@ -101,7 +102,7 @@ export const moveMediaWindow = ( } if (displayNr === undefined) return; fullscreen = - fullscreen ?? mediaWindowIsFullScreen(screens[displayNr].bounds); + fullscreen ?? mediaWindowIsFullScreen(screens[displayNr]?.bounds); } else { displayNr = 0; fullscreen = false; diff --git a/src-electron/main/window/window-state.ts b/src-electron/main/window/window-state.ts index 3a080951eb..c478cbf714 100644 --- a/src-electron/main/window/window-state.ts +++ b/src-electron/main/window/window-state.ts @@ -5,7 +5,7 @@ import { type Rectangle, screen, } from 'electron'; -import { ensureDirSync, readJsonSync, writeJsonSync } from 'fs-extra'; +import { ensureDirSync, readJsonSync, writeJsonSync } from 'fs-extra/esm'; import { dirname, join } from 'path'; import { captureElectronError } from './../utils'; @@ -68,7 +68,7 @@ export class StatefulBrowserWindow { try { this.win.setBounds({ height, width, x, y }); - } catch (e) { + } catch { // This fails when opening the website window for some reason } @@ -211,7 +211,7 @@ function refineOptionsAndState( try { savedState = readJsonSync(join(configFilePath, configFileName)); - } catch (e) { + } catch { // Don't care, use defaults } diff --git a/src-electron/main/window/window-website.ts b/src-electron/main/window/window-website.ts index 915408008d..e4df67815e 100644 --- a/src-electron/main/window/window-website.ts +++ b/src-electron/main/window/window-website.ts @@ -110,7 +110,8 @@ export async function createWebsiteWindow(lang?: string) { if (PLATFORM === 'darwin') { websiteWindow.setAspectRatio(16 / 9, { - height: websiteWindow.getSize()[1] - websiteWindow.getContentSize()[1], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + height: websiteWindow.getSize()[1]! - websiteWindow.getContentSize()[1]!, width: 0, }); } else { @@ -190,10 +191,13 @@ const setAspectRatio = () => { // Compute the new aspect ratio that, when the frame is removed, results in a 16:9 aspect ratio for the content const size = websiteWindow.getSize(); const contentSize = websiteWindow.getContentSize(); - const frameSize = [size[0] - contentSize[0], size[1] - contentSize[1]]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const frameSize = [size[0]! - contentSize[0]!, size[1]! - contentSize[1]!]; const aspectRatio = 16 / 9; const newAspectRatio = - (contentSize[0] + frameSize[0]) / - (contentSize[0] / aspectRatio + frameSize[1]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (contentSize[0]! + frameSize[0]!) / + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (contentSize[0]! / aspectRatio + frameSize[1]!); websiteWindow.setAspectRatio(newAspectRatio); }; diff --git a/src-electron/preload/converters.ts b/src-electron/preload/converters.ts index 944e9e9046..ec1299f15f 100644 --- a/src-electron/preload/converters.ts +++ b/src-electron/preload/converters.ts @@ -2,9 +2,11 @@ import type Decompress from 'decompress'; import type { PDFPageProxy } from 'pdfjs-dist'; import type { RenderParameters } from 'pdfjs-dist/types/src/display/api'; -import { ensureDir, writeFile } from 'fs-extra'; +import fse from 'fs-extra'; +const { ensureDir, writeFile } = fse; import { FULL_HD } from 'src/constants/media'; -import { basename, join } from 'upath'; +import upath from 'upath'; +const { basename, join } = upath; import { capturePreloadError } from './log'; diff --git a/src-electron/preload/fs.ts b/src-electron/preload/fs.ts index e063cbd0e2..09ee0762e5 100644 --- a/src-electron/preload/fs.ts +++ b/src-electron/preload/fs.ts @@ -1,3 +1,5 @@ +// eslint-env node + import type { IOptions } from 'music-metadata'; import type { VideoDuration } from 'src/types'; @@ -14,6 +16,7 @@ export const getVideoDuration = async ( export const parseMediaFile = async (filePath: string, options?: IOptions) => { const musicMetadata = await import('music-metadata'); + // @ts-expect-error: parseFile does not exist in browser context return musicMetadata.parseFile(filePath, options); }; diff --git a/src-electron/preload/log.ts b/src-electron/preload/log.ts index 5e2bc19c23..9fa1305913 100644 --- a/src-electron/preload/log.ts +++ b/src-electron/preload/log.ts @@ -1,4 +1,4 @@ -import type { ExclusiveEventHintOrCaptureContext } from '@sentry/core/build/types/utils/prepareEvent'; +import type { ExclusiveEventHintOrCaptureContext } from 'app/node_modules/@sentry/core/build/types/utils/prepareEvent'; import { captureException } from '@sentry/electron/renderer'; diff --git a/src/boot/i18n.ts b/src/boot/i18n.ts index 4613d5e5d0..15091776cd 100644 --- a/src/boot/i18n.ts +++ b/src/boot/i18n.ts @@ -1,4 +1,4 @@ -import { boot } from 'quasar/wrappers'; +import { defineBoot } from '#q-app/wrappers'; import messages from 'src/i18n'; import { createI18n } from 'vue-i18n'; @@ -7,7 +7,7 @@ export type MessageSchema = (typeof messages)['en']; let i18n: ReturnType = createI18n({}); -export default boot(({ app }) => { +export default defineBoot(({ app }) => { i18n = createI18n({ fallbackLocale: 'en', legacy: false, diff --git a/src/boot/sentry.ts b/src/boot/sentry.ts index 3e809923c9..15f2fba95e 100644 --- a/src/boot/sentry.ts +++ b/src/boot/sentry.ts @@ -1,17 +1,17 @@ +import { defineBoot } from '#q-app/wrappers'; import { init, replayIntegration } from '@sentry/electron/renderer'; import { browserTracingIntegration, init as initVue, vueIntegration, } from '@sentry/vue'; -import { boot } from 'quasar/wrappers'; import { IS_DEV } from 'src/constants/general'; import { SENTRY_DSN } from 'src/constants/sentry'; import { errorCatcher } from 'src/helpers/error-catcher'; import { version } from '../../package.json'; -export default boot(({ app, router }) => { +export default defineBoot(({ app, router }) => { try { if (!IS_DEV) { init( diff --git a/src/components/dialog/DialogAudioBible.vue b/src/components/dialog/DialogAudioBible.vue index d38844e4d3..2f6fbaf7f8 100644 --- a/src/components/dialog/DialogAudioBible.vue +++ b/src/components/dialog/DialogAudioBible.vue @@ -252,7 +252,7 @@ const selectedBookMedia = computed(() => { (item) => item?.booknum === selectedBibleBook.value, )?.files || {}, ); - const langFiles: PublicationFiles = allFiles[0]; + const langFiles: PublicationFiles = allFiles[0] ?? {}; const mediaFiles: MediaLink[] = Object.values(langFiles || {})[0]; return mediaFiles || []; }); @@ -298,7 +298,8 @@ const totalChosenVerses = computed(() => { ? '' : ' (' + (chosenVerses.value.length === 2 - ? Math.abs(chosenVerses.value[0] - chosenVerses.value[1]) + 1 + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Math.abs(chosenVerses.value[0]! - chosenVerses.value[1]!) + 1 : chosenVerses.value.length === 1 ? 1 : '') + @@ -338,7 +339,7 @@ const addSelectedVerses = async () => { const timeToSeconds = (time: string) => { const [h, m, s] = time.split(':').map(parseFloat); - return h * 3600 + m * 60 + s; + return (h ?? 0) * 3600 + (m ?? 0) * 60 + (s ?? 0); }; const min = timeToSeconds( @@ -409,7 +410,7 @@ const getVerseClass = (verse: number) => { return 'bg-primary-semi-transparent text-white'; } else if ( chosenVerses.value.length === 1 && - chosenVerses.value !== null && + chosenVerses.value?.[0] && verse > Math.min(chosenVerses.value[0], hoveredVerse.value || 0) && verse < Math.max(chosenVerses.value[0], hoveredVerse.value || 0) ) { diff --git a/src/components/dialog/DialogBackgroundMusicPopup.vue b/src/components/dialog/DialogBackgroundMusicPopup.vue index 414234052f..5764a1d7a7 100644 --- a/src/components/dialog/DialogBackgroundMusicPopup.vue +++ b/src/components/dialog/DialogBackgroundMusicPopup.vue @@ -340,7 +340,7 @@ const getNextSong = async () => { } return null; }) - .filter((song) => song !== null); + .filter((song) => !!song); if (timeBeforeMeetingStart > 0) { let customSongList: SongItem[] = []; if (selectedDaySongs.length) { @@ -371,7 +371,7 @@ const getNextSong = async () => { nextSongUrl: '', secsFromEnd: 0, }; - let nextSong = songList.value.shift(); + const nextSong = songList.value.shift(); if (!nextSong) { return { nextSongUrl: '', @@ -418,7 +418,6 @@ const fadeToVolumeLevel = (targetVolume: number, fadeOutSeconds: number) => { const volumeChange = targetVolume - initialVolume; const startTime = performance.now(); - // eslint-disable-next-line no-inner-declarations function updateVolume(currentTime: number) { try { if (!musicPlayer.value) return; diff --git a/src/components/dialog/DialogDisplayPopup.vue b/src/components/dialog/DialogDisplayPopup.vue index 7c62b33375..b3f6646382 100644 --- a/src/components/dialog/DialogDisplayPopup.vue +++ b/src/components/dialog/DialogDisplayPopup.vue @@ -279,11 +279,11 @@ const chooseCustomBackground = async (reset?: boolean) => { try { const backgroundPicker = await openFileDialog(true, 'jwpub+image+pdf'); if (backgroundPicker?.canceled) return; - if (!backgroundPicker || backgroundPicker.filePaths?.length === 0) { + if (!backgroundPicker?.filePaths.length) { notifyInvalidBackgroundFile(); } else { const filepath = backgroundPicker.filePaths[0]; - if (isJwpub(filepath)) { + if (filepath && isJwpub(filepath)) { jwpubImportFilePath.value = filepath; const unzipDir = await decompressJwpub(filepath); const db = await findDb(unzipDir); @@ -300,7 +300,7 @@ const chooseCustomBackground = async (reset?: boolean) => { if (jwpubImages.value?.length === 0) { notifyInvalidBackgroundFile(); } - } else { + } else if (filepath) { const tempDirectory = await getTempPath(); const tempFilepath = path.join( tempDirectory, diff --git a/src/components/dialog/DialogStudyBible.vue b/src/components/dialog/DialogStudyBible.vue index a0e8c72db1..327c443363 100644 --- a/src/components/dialog/DialogStudyBible.vue +++ b/src/components/dialog/DialogStudyBible.vue @@ -9,7 +9,7 @@
@@ -18,7 +18,7 @@ @@ -287,24 +287,27 @@ const bibleMediaCategories = ref([]); const bibleMediaByCategory = computed(() => { const returnObj: Record = {}; - returnObj[bibleMediaCategories.value[0]] = allBibleMedia.value.filter( - (item) => item.DocumentId < bibleBooksStartAtId.value, - ); - returnObj[bibleMediaCategories.value[1]] = allBibleMedia.value.filter( - (item) => item.BookNumber, - ); + returnObj[bibleMediaCategories.value[0] ?? 'INTRODUCTION'] = + allBibleMedia.value.filter( + (item) => item.DocumentId < bibleBooksStartAtId.value, + ); + returnObj[bibleMediaCategories.value[1] ?? 'BOOKS'] = + allBibleMedia.value.filter((item) => item.BookNumber); for (let i = 2; i < bibleMediaCategories.value.length; i++) { - returnObj[bibleMediaCategories.value[i]] = allBibleMedia.value.filter( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + returnObj[bibleMediaCategories.value[i]!] = allBibleMedia.value.filter( (item) => item.DocumentId > bibleBooksEndAtId.value && (item.ParentTitle || '').toLowerCase() === - bibleMediaCategories.value[i].toLowerCase(), + bibleMediaCategories.value[i]?.toLowerCase(), ); } return returnObj; }); const bibleBookMedia = computed(() => { - return bibleMediaByCategory.value[bibleMediaCategories.value[1]]; + return ( + bibleMediaByCategory.value[bibleMediaCategories.value[1] ?? 'BOOKS'] ?? [] + ); }); const bibleBooks = ref>({}); @@ -321,8 +324,9 @@ const selectedBookChapters = computed(() => { }); const selectedChapterMediaItems = computed(() => { - if (tab.value !== bibleMediaCategories.value[1]) + if (tab.value !== bibleMediaCategories.value[1]) { return bibleMediaByCategory.value[tab.value]; + } if (!bibleBook.value || !bibleBookChapter.value) return []; const filteredItems = bibleBookMedia.value.filter( @@ -351,7 +355,8 @@ const selectedChapterMediaItems = computed(() => { let start = sorted[0]; for (let i = 1; i <= sorted.length; i++) { - if (sorted[i] !== sorted[i - 1] + 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (sorted[i] !== sorted[i - 1]! + 1) { ranges.push( start === sorted[i - 1] ? `${start}` : `${start}-${sorted[i - 1]}`, ); @@ -371,20 +376,21 @@ const selectedChapterMediaItems = computed(() => { }); const groupedMediaItems = computed(() => { - const groups = selectedChapterMediaItems.value.reduce( - (groups: Record, item) => { - const label = - (tab.value !== bibleMediaCategories.value[1] - ? item.Title - : item.FormattedVerseLabel) || ''; - if (!groups[label]) { - groups[label] = []; - } - groups[label].push(item); - return groups; - }, - {}, - ); + const groups = + selectedChapterMediaItems.value?.reduce( + (groups: Record, item) => { + const label = + (tab.value !== bibleMediaCategories.value[1] + ? item.Title + : item.FormattedVerseLabel) || ''; + if (!groups[label]) { + groups[label] = []; + } + groups[label].push(item); + return groups; + }, + {}, + ) ?? {}; if (tab.value !== bibleMediaCategories.value[1]) { Object.entries(groups).sort((a, b) => SORTER.compare(a[0], b[0])); } @@ -393,7 +399,7 @@ const groupedMediaItems = computed(() => { const [aStart] = parseRange(a[0]); const [bStart] = parseRange(b[0]); - return aStart - bStart; + return (aStart ?? 0) - (bStart ?? 0); }); }); @@ -420,8 +426,9 @@ const getBibleMediaCategories = async () => { } catch (error) { errorCatcher(error); } finally { - if (bibleMediaCategories.value.length > 1) + if (bibleMediaCategories.value[1]) { tab.value = bibleMediaCategories.value[1]; + } loading.value = false; } }; @@ -480,11 +487,11 @@ const addStudyBibleMedia = async (mediaItem: MultimediaItem) => { const langsToTry = [ ...new Set([ - /* eslint-disable perfectionist/sort-sets */ + currentSettings.value?.lang, currentSettings.value?.langFallback, 'E', - /* eslint-enable perfectionist/sort-sets */ + ]), ].filter((l) => l !== undefined && l !== null); let mediaInfo, mediaItemFiles; diff --git a/src/components/form-inputs/FolderInput.vue b/src/components/form-inputs/FolderInput.vue index b3ee50232a..99ec8578ca 100644 --- a/src/components/form-inputs/FolderInput.vue +++ b/src/components/form-inputs/FolderInput.vue @@ -36,7 +36,7 @@ const showFolderPicker = async () => { if (!result || !result.filePaths || result.canceled) { model.value = null; } else if (result.filePaths.length > 0) { - model.value = result.filePaths[0]; + model.value = result.filePaths[0] ?? null; } } catch (error) { errorCatcher(error); diff --git a/src/components/form-inputs/SelectInput.vue b/src/components/form-inputs/SelectInput.vue index b2b29b36d7..0a1ebf29ce 100644 --- a/src/components/form-inputs/SelectInput.vue +++ b/src/components/form-inputs/SelectInput.vue @@ -167,7 +167,7 @@ const listOptions = computed( return RESOLUTIONS.map((r) => ({ label: r, value: r })); } else if (props.list === 'days') { return Array.from({ length: 7 }, (_, i) => ({ - label: dateLocale.value.days[i === 6 ? 0 : i + 1], + label: dateLocale.value.days[i === 6 ? 0 : i + 1] ?? '', value: String(i), })); } else if (props.list?.startsWith('obs')) { diff --git a/src/components/header/HeaderCalendar.vue b/src/components/header/HeaderCalendar.vue index 0497973ac1..a989ea6de2 100644 --- a/src/components/header/HeaderCalendar.vue +++ b/src/components/header/HeaderCalendar.vue @@ -120,7 +120,7 @@ - {{ $t(name) }} + {{ $t(name ? name : '') }} {{ $t(name + '-explain') }} @@ -347,6 +347,7 @@ const minDate = () => { const dateArray: Date[] = lookupPeriod.value[currentCongregation.value]?.map((day) => day.date) || []; + if (!dateArray[0]) return undefined; const minDate = getMinDate(dateArray[0], ...dateArray.slice(1)); return formatDate(minDate, 'YYYY/MM'); } catch (error) { @@ -361,6 +362,7 @@ const maxDate = () => { const dateArray: Date[] = lookupPeriod.value[currentCongregation.value]?.map((day) => day.date) || []; + if (!dateArray[0]) return undefined; const maxDate = getMaxDate(dateArray[0], ...dateArray.slice(1)); return formatDate(maxDate, 'YYYY/MM'); } catch (error) { @@ -381,6 +383,7 @@ const dateOptions = (lookupDate: string) => { const dateArray: Date[] = lookupPeriod.value[currentCongregation.value]?.map((day) => day.date) || []; + if (!dateArray[0]) return true; const minDate = getMinDate(dateArray[0], ...dateArray.slice(1)); const maxDate = getMaxDate(dateArray[0], ...dateArray.slice(1)); return ( diff --git a/src/components/header/HeaderSettings.vue b/src/components/header/HeaderSettings.vue index 75195e75fd..90b87a42ad 100644 --- a/src/components/header/HeaderSettings.vue +++ b/src/components/header/HeaderSettings.vue @@ -177,11 +177,7 @@ const unusedParentDirectories = computed(() => { !frequentlyUsedDirectories.value.has(file.parentPath) && !untouchableDirectories.value.has(file.parentPath) ) { - if (acc[file.parentPath]) { - acc[file.parentPath] += file.size; - } else { - acc[file.parentPath] = file.size; - } + acc[file.parentPath] = file.size + (acc[file.parentPath] ?? 0); } return acc; }, {}); @@ -194,11 +190,7 @@ const unusedParentDirectories = computed(() => { const usedParentDirectories = computed(() => { try { return usedCacheFiles.value.reduce>((acc, file) => { - if (acc[file.parentPath]) { - acc[file.parentPath] += file.size; - } else { - acc[file.parentPath] = file.size; - } + acc[file.parentPath] = file.size + (acc[file.parentPath] ?? 0); return acc; }, {}); } catch (error) { diff --git a/src/components/media/MediaItem.vue b/src/components/media/MediaItem.vue index 0a06f24fb9..106be22018 100644 --- a/src/components/media/MediaItem.vue +++ b/src/components/media/MediaItem.vue @@ -129,7 +129,7 @@ @mouseenter="setHoveredBadge(media.uniqueId, true)" @mouseleave="setHoveredBadge(media.uniqueId, false)" > -