chore: back to electron + nx

This commit is contained in:
4gray committed 2025-10-07 18:02:13 +02:00
1 parent f5b3a15b83
commit 99ed5df404
477 files changed
+19213 -17370

No files matched your search

+2 -1
View File
@@ -28,7 +28,8 @@ If applicable, add screenshots to help explain your problem.
- PWA or Electron/Tauri application
- OS: [e.g. Linux]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- Version [e.g. 0.16.0]
- Feature: [e.g. M3U playlist, Xtream Code, Stalker Portal]
**Additional context**
Add any other context about the problem here.
-69
View File
@@ -1,69 +0,0 @@
name: 'Tauri Release'
on:
push:
branches:
- master
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest' # for Arm based macs (M1 and above).
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest' # for Intel based macs.
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm' # Set this to npm, yarn or pnpm.
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: install frontend dependencies
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
run: npm install # change this to npm or pnpm depending on which one you use.
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: 'App v__VERSION__'
releaseBody: 'See the assets to download this version and install.'
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
-19
View File
@@ -1,19 +0,0 @@
{
"branches": [
"master"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/changelog", {
"changelogFile": "CHANGELOG.md"
}],
"@semantic-release/npm", {
"npmPublish": false,
},
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}],
]
}
-166
View File
@@ -1,166 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"schematicCollections": ["@angular-eslint/schematics"],
"analytics": false
},
"version": 1,
"newProjectRoot": "projects",
"projects": {
"iptvnator": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true,
"style": "scss"
}
},
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": {
"base": "dist"
},
"index": "src/index.html",
"tsConfig": "src/tsconfig.app.json",
"polyfills": ["zone.js"],
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/m3-theme.scss",
"./node_modules/video.js/dist/video-js.css"
],
"scripts": [],
"serviceWorker": "ngsw-config.json",
"webWorkerTsConfig": "tsconfig.worker.json",
"browser": "src/main.ts"
},
"configurations": {
"dev": {
"optimization": false,
"outputHashing": "none",
"sourceMap": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dev.ts"
}
]
},
"web": {
"optimization": true,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.web.ts"
}
]
},
"production": {
"optimization": false,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"buildTarget": "iptvnator:build"
},
"configurations": {
"dev": {
"buildTarget": "iptvnator:build:dev"
},
"web": {
"buildTarget": "iptvnator:build:web"
},
"production": {
"buildTarget": "iptvnator:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "iptvnator:build"
}
},
"test": {
"builder": "@angular-builders/jest:run",
"options": {}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
},
"iptvnator-e2e": {
"root": "e2e",
"projectType": "application",
"architect": {
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["e2e/**/*.ts"]
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"style": "scss",
"type": "component"
},
"@schematics/angular:directive": {
"prefix": "app",
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
+3
View File
@@ -0,0 +1,3 @@
import baseConfig from '../../eslint.config.mjs';
export default [...baseConfig];
+13
View File
@@ -0,0 +1,13 @@
export default {
displayName: 'electron-backend',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/electron-backend',
};
+83
View File
@@ -0,0 +1,83 @@
{
"name": "electron-backend",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/electron-backend/src",
"projectType": "application",
"targets": {
"build": {
"dependsOn": [
{
"projects": ["web"],
"target": "build"
}
],
"executor": "nx-electron:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/electron-backend",
"main": "apps/electron-backend/src/main.ts",
"tsConfig": "apps/electron-backend/tsconfig.app.json",
"assets": ["apps/electron-backend/src/assets"]
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false,
"fileReplacements": [
{
"replace": "apps/electron-backend/src/environments/environment.ts",
"with": "apps/electron-backend/src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"executor": "nx:run-commands",
"options": {
"commands": [
"nx run web:serve",
"nx run electron-backend:serve-electron"
]
}
},
"serve-electron": {
"executor": "nx-electron:execute",
"options": {
"buildTarget": "electron-backend:build"
}
},
"package": {
"dependsOn": ["electron-backend:build"],
"executor": "nx-electron:package",
"options": {
"name": "electron-backend",
"frontendProject": "web",
"sourcePath": "dist/apps",
"outputPath": "dist/packages",
"prepackageOnly": true
}
},
"make": {
"dependsOn": ["electron-backend:build"],
"executor": "nx-electron:make",
"options": {
"name": "electron-backend",
"frontendProject": "web",
"sourcePath": "dist/apps",
"outputPath": "dist/executables"
}
},
"lint": {
"command": "eslint apps/electron-backend/**/*.ts"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/electron-backend/jest.config.ts"
}
}
}
}
@@ -0,0 +1,6 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electron', {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
platform: process.platform,
});
+134
View File
@@ -0,0 +1,134 @@
import { BrowserWindow, shell, screen } from 'electron';
import { rendererAppName, rendererAppPort } from './constants';
import { environment } from '../environments/environment';
import { join } from 'path';
import { format } from 'url';
export default class App {
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
static mainWindow: Electron.BrowserWindow;
static application: Electron.App;
static BrowserWindow;
public static isDevelopmentMode() {
const isEnvironmentSet: boolean = 'ELECTRON_IS_DEV' in process.env;
const getFromEnvironment: boolean =
parseInt(process.env.ELECTRON_IS_DEV, 10) === 1;
return isEnvironmentSet ? getFromEnvironment : !environment.production;
}
private static onWindowAllClosed() {
if (process.platform !== 'darwin') {
App.application.quit();
}
}
private static onClose() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
App.mainWindow = null;
}
private static onRedirect(event: any, url: string) {
if (url !== App.mainWindow.webContents.getURL()) {
// this is a normal external redirect, open it in a new browser window
event.preventDefault();
shell.openExternal(url);
}
}
private static onReady() {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
if (rendererAppName) {
App.initMainWindow();
App.loadMainWindow();
}
}
private static onActivate() {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (App.mainWindow === null) {
App.onReady();
}
}
private static initMainWindow() {
const workAreaSize = screen.getPrimaryDisplay().workAreaSize;
const width = Math.min(1280, workAreaSize.width || 1280);
const height = Math.min(720, workAreaSize.height || 720);
// Create the browser window.
App.mainWindow = new BrowserWindow({
width: width,
height: height,
show: false,
webPreferences: {
contextIsolation: true,
backgroundThrottling: false,
preload: join(__dirname, 'main.preload.js'),
},
});
App.mainWindow.setMenu(null);
App.mainWindow.center();
// if main window is ready to show, close the splash window and show the main window
App.mainWindow.once('ready-to-show', () => {
App.mainWindow.show();
});
// handle all external redirects in a new browser window
// App.mainWindow.webContents.on('will-navigate', App.onRedirect);
// App.mainWindow.webContents.on('new-window', (event, url, frameName, disposition, options) => {
// App.onRedirect(event, url);
// });
// Emitted when the window is closed.
App.mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
App.mainWindow = null;
});
}
private static loadMainWindow() {
// load the index.html of the app.
if (!App.application.isPackaged) {
App.mainWindow.loadURL(`http://localhost:${rendererAppPort}`);
App.mainWindow.webContents.openDevTools();
} else {
App.mainWindow.loadURL(
format({
pathname: join(
__dirname,
'..',
rendererAppName,
'index.html'
),
protocol: 'file:',
slashes: true,
})
);
}
}
static main(app: Electron.App, browserWindow: typeof BrowserWindow) {
// we pass the Electron.App object and the
// Electron.BrowserWindow into this function
// so this class has no dependencies. This
// makes the code easier to write tests for
App.BrowserWindow = browserWindow;
App.application = app;
App.application.on('window-all-closed', App.onWindowAllClosed); // Quit when all windows are closed.
App.application.on('ready', App.onReady); // App is ready to load data
App.application.on('activate', App.onActivate); // App is activated
}
}
@@ -0,0 +1,4 @@
export const rendererAppPort = 4200;
export const rendererAppName = 'web'; // options.name.split('-')[0] + '-web'
export const electronAppName = 'iptvnator';
export const updateServerUrl = 'https://deployment-server-url.com'; // TODO: insert your update server url here
@@ -0,0 +1,25 @@
/**
* This module is responsible on handling all the inter process communications
* between the frontend to the electron backend.
*/
import { app, ipcMain } from 'electron';
import { environment } from '../../environments/environment';
export default class ElectronEvents {
static bootstrapElectronEvents(): Electron.IpcMain {
return ipcMain;
}
}
// Retrieve app version
ipcMain.handle('get-app-version', (event) => {
console.log(`Fetching application version... [v${environment.version}]`);
return environment.version;
});
// Handle App termination
ipcMain.on('quit', (event, code) => {
app.exit(code);
});
@@ -0,0 +1,73 @@
/**
* This module is responsible on handling all the setup events that is submitted by squirrel.
*/
import { app } from 'electron';
import { spawn } from 'child_process';
import { resolve, join, basename } from 'path';
import { environment } from '../../environments/environment';
export default class SquirrelEvents {
private static isAppFirstRun = false;
// app paths
private static appFolder = resolve(process.execPath, '..');
private static appRootFolder = resolve(SquirrelEvents.appFolder, '..');
private static updateExe = resolve(
join(SquirrelEvents.appRootFolder, 'Update.exe')
);
private static exeName = resolve(
join(
SquirrelEvents.appRootFolder,
'app-' + environment.version,
basename(process.execPath)
)
);
static handleEvents(): boolean {
if (process.argv.length === 1 || process.platform !== 'win32') {
return false;
}
switch (process.argv[1]) {
case '--squirrel-install':
case '--squirrel-updated':
// Install desktop and start menu shortcuts
SquirrelEvents.update(['--createShortcut', SquirrelEvents.exeName]);
return true;
case '--squirrel-uninstall':
// Remove desktop and start menu shortcuts
SquirrelEvents.update(['--removeShortcut', SquirrelEvents.exeName]);
return true;
case '--squirrel-obsolete':
app.quit();
return true;
case '--squirrel-firstrun':
// Check if it the first run of the software
SquirrelEvents.isAppFirstRun = true;
return false;
}
return false;
}
static isFirstRun(): boolean {
return SquirrelEvents.isAppFirstRun;
}
private static update(args: Array<string>) {
try {
spawn(SquirrelEvents.updateExe, args, { detached: true }).on(
'close',
() => setTimeout(app.quit, 1000)
);
} catch (error) {
setTimeout(app.quit, 1000);
}
}
}
@@ -0,0 +1,69 @@
import { app, autoUpdater, dialog, MessageBoxOptions } from 'electron';
import { platform, arch } from 'os';
import { updateServerUrl } from '../constants';
import App from '../app';
export default class UpdateEvents {
// initialize auto update service - most be invoked only in production
static initAutoUpdateService() {
const platform_arch =
platform() === 'win32' ? platform() : platform() + '_' + arch();
const version = app.getVersion();
const feed: Electron.FeedURLOptions = {
url: `${updateServerUrl}/update/${platform_arch}/${version}`,
};
if (!App.isDevelopmentMode()) {
console.log('Initializing auto update service...\n');
autoUpdater.setFeedURL(feed);
UpdateEvents.checkForUpdates();
}
}
// check for updates - most be invoked after initAutoUpdateService() and only in production
static checkForUpdates() {
if (!App.isDevelopmentMode() && autoUpdater.getFeedURL() !== '') {
autoUpdater.checkForUpdates();
}
}
}
autoUpdater.on(
'update-downloaded',
(event, releaseNotes, releaseName, releaseDate) => {
const dialogOpts: MessageBoxOptions = {
type: 'info' as const,
buttons: ['Restart', 'Later'],
title: 'Application Update',
message: process.platform === 'win32' ? releaseNotes : releaseName,
detail:
'A new version has been downloaded. Restart the application to apply the updates.',
};
dialog.showMessageBox(dialogOpts).then((returnValue) => {
if (returnValue.response === 0) autoUpdater.quitAndInstall();
});
}
);
autoUpdater.on('checking-for-update', () => {
console.log('Checking for updates...\n');
});
autoUpdater.on('update-available', () => {
console.log('New update available!\n');
});
autoUpdater.on('update-not-available', () => {
console.log('Up to date!\n');
});
autoUpdater.on('before-quit-for-update', () => {
console.log('Application update is about to begin...\n');
});
autoUpdater.on('error', (message) => {
console.error('There was a problem updating the application');
console.error(message, '\n');
});
@@ -0,0 +1,3 @@
{
"$schema": "../../../../../node_modules/nx-electron/src/validation/maker.schema.json"
}
@@ -0,0 +1 @@
Assets Folder
@@ -0,0 +1,6 @@
declare const __BUILD_VERSION__: string;
export const environment = {
production: true,
version: __BUILD_VERSION__,
};
@@ -0,0 +1,6 @@
declare const __BUILD_VERSION__: string;
export const environment = {
production: false,
version: __BUILD_VERSION__,
};
+34
View File
@@ -0,0 +1,34 @@
import SquirrelEvents from './app/events/squirrel.events';
import ElectronEvents from './app/events/electron.events';
import UpdateEvents from './app/events/update.events';
import { app, BrowserWindow } from 'electron';
import App from './app/app';
export default class Main {
static initialize() {
if (SquirrelEvents.handleEvents()) {
// squirrel event handled (except first run event) and app will exit in 1000ms, so don't do anything else
app.quit();
}
}
static bootstrapApp() {
App.main(app, BrowserWindow);
}
static bootstrapAppEvents() {
ElectronEvents.bootstrapElectronEvents();
// initialize auto updater service
if (!App.isDevelopmentMode()) {
// UpdateEvents.initAutoUpdateService();
}
}
}
// handle setup events as quickly as possible
Main.initialize();
// bootstrap app
Main.bootstrapApp();
Main.bootstrapAppEvents();
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
],
"include": ["**/*.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["**/*.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
+12
View File
@@ -0,0 +1,12 @@
import playwright from 'eslint-plugin-playwright';
import baseConfig from '../../eslint.config.mjs';
export default [
playwright.configs['flat/recommended'],
...baseConfig,
{
files: ['**/*.ts', '**/*.js'],
// Override or add rules here
rules: {},
},
];
+68
View File
@@ -0,0 +1,68 @@
import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run web:serve',
url: 'http://localhost:4200',
reuseExistingServer: true,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
} */
],
});
+9
View File
@@ -0,0 +1,9 @@
{
"name": "web-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/web-e2e/src",
"implicitDependencies": ["web"],
"// targets": "to see all targets run: nx show project web-e2e --web",
"targets": {}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"module": "commonjs",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"**/*.ts",
"**/*.js",
"playwright.config.ts",
"src/**/*.spec.ts",
"src/**/*.spec.js",
"src/**/*.test.ts",
"src/**/*.test.js",
"src/**/*.d.ts"
]
}
+34
View File
@@ -0,0 +1,34 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
+21
View File
@@ -0,0 +1,21 @@
export default {
displayName: 'web',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../coverage/apps/web',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};
+99
View File
@@ -0,0 +1,99 @@
{
"name": "web",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/web/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular/build:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": {
"base": "dist/apps/web",
"browser": ""
},
"polyfills": ["zone.js"],
"tsConfig": "apps/web/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/web/src/favicon.ico",
"apps/web/src/assets",
"apps/web/src/manifest.webmanifest"
],
"styles": [
"apps/web/src/styles.scss",
"apps/web/src/m3-theme.scss",
"./node_modules/video.js/dist/video-js.css"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "3mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"continuous": true,
"executor": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "web:build:production"
},
"development": {
"buildTarget": "web:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"proxyConfig": "apps/web/proxy.conf.json"
}
},
"extract-i18n": {
"executor": "@angular/build:extract-i18n",
"options": {
"buildTarget": "web:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/web/jest.config.ts",
"tsConfig": "apps/web/tsconfig.spec.json"
}
},
"serve-static": {
"continuous": true,
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "web:build",
"port": 4200,
"staticFilePath": "dist/apps/web/browser",
"spa": true
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,28 +1,21 @@
import { Component } from '@angular/core';
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { isTauri } from '@tauri-apps/api/core';
import { ask } from '@tauri-apps/plugin-dialog';
import { relaunch } from '@tauri-apps/plugin-process';
import { check } from '@tauri-apps/plugin-updater';
import { firstValueFrom } from 'rxjs';
import * as semver from 'semver';
import { IpcCommand } from '../../shared/ipc-command.class';
import { IpcCommand } from '../shared/ipc-command.class';
import {
AUTO_UPDATE_PLAYLISTS,
ERROR,
OPEN_FILE,
SETTINGS_UPDATE,
SHOW_WHATS_NEW,
VIEW_ADD_PLAYLIST,
VIEW_SETTINGS,
} from '../../shared/ipc-commands';
} from '../shared/ipc-commands';
import { DataService } from './services/data.service';
import { EpgService } from './services/epg.service';
import { PlaylistsService } from './services/playlists.service';
import { SettingsService } from './services/settings.service';
import { WhatsNewService } from './services/whats-new.service';
import { Language } from './settings/language.enum';
@@ -38,18 +31,28 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res
templateUrl: './app.component.html',
standalone: false,
})
export class AppComponent {
export class AppComponent implements OnInit, OnDestroy {
private dataService = inject(DataService);
private dialog = inject(MatDialog);
private epgService = inject(EpgService);
private router = inject(Router);
private store = inject(Store);
private snackBar = inject(MatSnackBar);
private translate = inject(TranslateService);
private settingsService = inject(SettingsService);
private whatsNewService = inject(WhatsNewService);
/** Visibility flag of the "what is new" modal dialog */
isDialogVisible$ = this.whatsNewService.dialogState$;
readonly isDialogVisible$ = this.whatsNewService.dialogState$;
/** Dialog options */
options = this.whatsNewService.options;
readonly options = this.whatsNewService.options;
/** Modals to show for the updated version of the application */
modals: any[] = [];
/** List of ipc commands with function mapping */
commandsList = [
private readonly commandsList = [
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()),
@@ -59,22 +62,11 @@ export class AppComponent {
];
/** Default language as fallback */
DEFAULT_LANG = Language.ENGLISH;
private readonly DEFAULT_LANG = Language.ENGLISH;
listeners = [];
private listeners = [];
constructor(
private dataService: DataService,
private dialog: MatDialog,
private epgService: EpgService,
private playlistService: PlaylistsService,
private router: Router,
private store: Store,
private snackBar: MatSnackBar,
private translate: TranslateService,
private settingsService: SettingsService,
private whatsNewService: WhatsNewService
) {
constructor() {
if (
((this.dataService.isElectron &&
this.dataService?.remote?.process.platform === 'linux') ||
@@ -94,7 +86,7 @@ export class AppComponent {
});
}
}
if (isTauri()) {
/* if (isTauri()) {
document.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.metaKey) {
if (event.key === 'f') {
@@ -106,7 +98,7 @@ export class AppComponent {
}
}
});
}
} */
}
ngOnInit() {
@@ -116,64 +108,6 @@ export class AppComponent {
this.setRendererListeners();
this.initSettings();
this.handleWhatsNewDialog();
this.triggerAutoUpdateMechanism();
this.checkForUpdates();
}
async checkForUpdates() {
if (isTauri()) {
const update = await check();
if (update?.available) {
console.log(
`found update ${update.version} from ${update.date} with notes ${update.body}`
);
let downloaded = 0;
let contentLength = 0;
const wantsUpdate = await ask(
`New version ${update.version} is available. Do you want to update now?`
);
if (wantsUpdate) {
await update.downloadAndInstall((event) => {
switch (event.event) {
case 'Started':
contentLength = event.data.contentLength;
console.log(
`started downloading ${event.data.contentLength} bytes`
);
break;
case 'Progress':
downloaded += event.data.chunkLength;
console.log(
`downloaded ${downloaded} from ${contentLength}`
);
break;
case 'Finished':
console.log('download finished');
break;
}
});
console.log('update installed');
await relaunch();
}
}
}
}
async triggerAutoUpdateMechanism() {
if (isTauri()) {
const playlistForAutoUpdate = await firstValueFrom(
this.playlistService.getPlaylistsForAutoUpdate()
);
if (playlistForAutoUpdate && playlistForAutoUpdate.length > 0)
this.dataService.sendIpcEvent(
AUTO_UPDATE_PLAYLISTS,
playlistForAutoUpdate
);
}
}
/**
@@ -202,13 +136,13 @@ export class AppComponent {
if (settings && Object.keys(settings).length > 0) {
this.dataService.sendIpcEvent(SETTINGS_UPDATE, settings);
this.translate.use(settings.language ?? this.DEFAULT_LANG);
if (
/* if (
settings.epgUrl?.length > 0 &&
settings.epgUrl?.some((u) => u !== '') &&
isTauri()
) {
this.epgService.fetchEpg(settings.epgUrl);
}
} */
if (settings.theme) {
this.settingsService.changeTheme(settings.theme);
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -4,8 +4,8 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { ERROR, PLAYLIST_PARSE_RESPONSE } from '../../../shared/ipc-commands';
import { Playlist } from '../../../shared/playlist.interface';
import { ERROR, PLAYLIST_PARSE_RESPONSE } from '../../shared/ipc-commands';
import { Playlist } from '../../shared/playlist.interface';
import { DataService } from '../services/data.service';
import { HeaderComponent } from '../shared/components/header/header.component';
import { addPlaylist } from '../state/actions';
@@ -20,7 +20,7 @@ import { isTauri } from '@tauri-apps/api/core';
import { save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs';
import { firstValueFrom } from 'rxjs';
import { Playlist } from '../../../../../shared/playlist.interface';
import { Playlist } from '../../../../shared/playlist.interface';
import { DatabaseService } from '../../../services/database.service';
import { PlaylistsService } from '../../../services/playlists.service';
import { PlaylistMeta } from '../../../shared/playlist-meta.type';
@@ -13,7 +13,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { MockComponent, MockModule, MockProvider } from 'ng-mocks';
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
import { Observable } from 'rxjs';
import { PLAYLIST_UPDATE } from '../../../../shared/ipc-commands';
import { PLAYLIST_UPDATE } from '../../../shared/ipc-commands';
import { DataService } from '../../services/data.service';
import { DialogService } from '../../services/dialog.service';
import { ElectronServiceStub } from '../../services/electron.service.stub';
@@ -7,6 +7,7 @@ import {
HostListener,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
@@ -22,8 +23,8 @@ import { Store } from '@ngrx/store';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
import { BehaviorSubject, combineLatest, map } from 'rxjs';
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../../shared/constants';
import { Playlist } from '../../../../shared/playlist.interface';
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants';
import { Playlist } from '../../../shared/playlist.interface';
import { DataService } from '../../services/data.service';
import { DatabaseService } from '../../services/database.service';
import { SortService } from '../../services/sort.service';
@@ -39,7 +40,7 @@ import {
MIGRATE_PLAYLISTS,
PLAYLIST_UPDATE,
PLAYLIST_UPDATE_RESPONSE,
} from './../../../../shared/ipc-commands';
} from './../../../shared/ipc-commands';
import { DialogService } from './../../services/dialog.service';
import { PlaylistMeta } from './../../shared/playlist-meta.type';
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
@@ -61,7 +62,7 @@ import { PlaylistItemComponent } from './playlist-item/playlist-item.component';
TranslatePipe,
],
})
export class RecentPlaylistsComponent implements OnDestroy {
export class RecentPlaylistsComponent implements OnInit, OnDestroy {
@ViewChild('searchQuery') searchQueryInput!: ElementRef<HTMLInputElement>;
readonly searchQuery = new BehaviorSubject('');
@@ -289,8 +290,8 @@ export class RecentPlaylistsComponent implements OnDestroy {
this.searchQuery.next(searchQuery);
}
@HostListener('window:keydown.control.f', ['$event'])
@HostListener('window:keydown.meta.f', ['$event'])
/* @HostListener('window:keydown.control.f', ['$event'])
@HostListener('window:keydown.meta.f', ['$event']) */
onSearchHotkey(event: KeyboardEvent) {
// Prevent default browser search behavior
event.preventDefault();
@@ -12,7 +12,7 @@ import { MatInputModule } from '@angular/material/input';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { v4 as uuid } from 'uuid';
import { Playlist } from '../../../../shared/playlist.interface';
import { Playlist } from '../../../shared/playlist.interface';
import { DataService } from '../../services/data.service';
import { addPlaylist } from '../../state/actions';
@@ -13,7 +13,7 @@ import { MatInputModule } from '@angular/material/input';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { v4 as uuid } from 'uuid';
import { Playlist } from '../../../../shared/playlist.interface';
import { Playlist } from '../../../shared/playlist.interface';
import {
PortalStatus,
PortalStatusService,
File renamed without changes.
@@ -9,7 +9,7 @@ import {
} from '@angular/core';
import Artplayer from 'artplayer';
import Hls from 'hls.js';
import { Channel } from '../../../../../shared/channel.interface';
import { Channel } from '../../../../shared/channel.interface';
Artplayer.AUTO_PLAYBACK_TIMEOUT = 10000;
@@ -25,7 +25,7 @@ import { Store } from '@ngrx/store';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import * as _ from 'lodash';
import { map, skipWhile } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import { Channel } from '../../../../shared/channel.interface';
import { EpgService } from '../../../services/epg.service';
import { FilterPipe } from '../../../shared/pipes/filter.pipe';
import * as PlaylistActions from '../../../state/actions';
@@ -11,7 +11,7 @@ import { TranslatePipe } from '@ngx-translate/core';
import moment from 'moment';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { EPG_GET_PROGRAM_DONE } from '../../../../shared/ipc-commands';
import { DataService } from '../../../services/data.service';
import { EpgService } from '../../../services/epg.service';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
@@ -9,9 +9,9 @@ import {
ViewChild,
} from '@angular/core';
import Hls from 'hls.js';
import { Channel } from '../../../../../shared/channel.interface';
import { CHANNEL_SET_USER_AGENT } from '../../../../../shared/ipc-commands';
import { getExtensionFromUrl } from '../../../../../shared/playlist.utils';
import { Channel } from '../../../../shared/channel.interface';
import { CHANNEL_SET_USER_AGENT } from '../../../../shared/ipc-commands';
import { getExtensionFromUrl } from '../../../../shared/playlist.utils';
import { DataService } from '../../../services/data.service';
/**
@@ -1,7 +1,7 @@
import { NgStyle } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import moment from 'moment';
import { Channel } from '../../../../../shared/channel.interface';
import { Channel } from '../../../../shared/channel.interface';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
import { EpgProgram } from '../../models/epg-program.model';
@@ -117,7 +117,7 @@
<rect
[attr.width]="program.width"
[attr.height]="barHeight"
[matTooltip]="program.title"
[matTooltip]="program.title[0].value"
></rect>
<foreignObject
[attr.width]="program.width"
@@ -20,7 +20,7 @@ import { TranslatePipe } from '@ngx-translate/core';
import { invoke } from '@tauri-apps/api/core';
import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns';
import { BehaviorSubject, Observable } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import { Channel } from '../../../../shared/channel.interface';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
import { EpgChannel } from '../../models/epg-channel.model';
import { EpgProgram } from '../../models/epg-program.model';
Loaded 100 of 477 files, more files were not shown because too many files have changed in this diff. Show more