Merge pull request #43 from 4gray/feature/refresh-playlists

Feature: Refresh playlists from fs or by url
This commit is contained in:
4gray authored and GitHub committed 2021-05-15 23:18:50 +02:00
commit 1f5688f4f7
35 files changed
+3143 -238

No files matched your search

+1
View File
@@ -11,6 +11,7 @@
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier/@typescript-eslint",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
+3
View File
@@ -4,6 +4,9 @@ on:
push:
branches:
- electron
pull_request:
branches:
- electron
jobs:
build-and-test:
+1
View File
@@ -3,5 +3,6 @@
[ -n "$CI" ] && exit 0
npx stylelint "**/*.scss"
npx --no-install commitlint --edit
npm test -- --coverage
+3
View File
@@ -1 +1,4 @@
*.yml
dist
coverage
release
+21
View File
@@ -0,0 +1,21 @@
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": ["ng-deep"]
}
],
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["function", "if", "each", "include", "mixin"]
}
],
"value-keyword-case": null
},
"plugins": ["stylelint-scss"],
"syntax": "scss"
}
-21
View File
@@ -1,21 +0,0 @@
os:
- linux
- osx
language: node_js
node_js:
- '12'
- '10'
dist: xenial
sudo: required
services:
- xvfb
before_script:
- export DISPLAY=:99.0
install:
- npm set progress=false
- npm install
script:
- ng lint
- npm run test
- npm run e2e
- npm run build
+178 -20
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import { app, BrowserWindow, ipcMain } from 'electron';
import { parse } from 'iptv-playlist-parser';
import axios from 'axios';
@@ -11,7 +10,12 @@ import {
EPG_FETCH_DONE,
EPG_GET_PROGRAM,
EPG_GET_PROGRAM_DONE,
ERROR,
PLAYLIST_SAVE_DETAILS,
PLAYLIST_PARSE,
PLAYLIST_PARSE_RESPONSE,
PLAYLIST_UPDATE,
PLAYLIST_UPDATE_RESPONSE,
} from './ipc-commands';
const fs = require('fs');
@@ -35,34 +39,40 @@ export class Api {
ipcMain.on('parse-playlist-by-url', async (event, args) => {
try {
await axios.get(args.url).then((result) => {
const array = result.data.split('\n');
const parsedPlaylist = this.parsePlaylist(array);
const parsedPlaylist = this.convertFileStringToPlaylist(
result.data
);
const playlistObject = this.createPlaylistObject(
args.title,
parsedPlaylist,
args.url
args.url,
'URL'
);
this.insertToDb(playlistObject);
event.sender.send('parse-response', {
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlistObject,
});
});
} catch (err) {
event.sender.send('error', {
event.sender.send(ERROR, {
message: err.response.statusText,
status: err.response.status,
});
}
});
ipcMain.on('parse-playlist', (event, args) => {
ipcMain.on(PLAYLIST_PARSE, (event, args) => {
const parsedPlaylist = this.parsePlaylist(args.playlist);
const playlistObject = this.createPlaylistObject(
args.title,
parsedPlaylist
parsedPlaylist,
args.path,
'FILE'
);
this.insertToDb(playlistObject);
event.sender.send('parse-response', { payload: playlistObject });
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlistObject,
});
});
ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event));
@@ -70,7 +80,7 @@ export class Api {
ipcMain.on('playlist-by-id', async (event, args) => {
const playlist = await db.findOne({ _id: args.id });
this.setUserAgent(playlist.userAgent);
event.sender.send('parse-response', {
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlist,
});
});
@@ -86,22 +96,23 @@ export class Api {
// open playlist from file system
ipcMain.on('open-file', (event, args) => {
fs.readFile(args.filePath, 'utf-8', (err, data) => {
fs.readFile(args.filePath, 'utf-8', (err, data: string) => {
if (err) {
console.log(
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
'An error ocurred reading the file :' + err.message
);
return;
}
const array = (data as string).split('\n');
const parsedPlaylist = this.parsePlaylist(array);
const parsedPlaylist = this.convertFileStringToPlaylist(data);
const playlistObject = this.createPlaylistObject(
args.fileName,
parsedPlaylist
parsedPlaylist,
args.filePath,
'FILE'
);
this.insertToDb(playlistObject);
event.sender.send('parse-response', {
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlistObject,
});
});
@@ -138,13 +149,30 @@ export class Api {
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
const updated = await db.update(
{ _id: args._id },
{ $set: { title: args.title, userAgent: args.userAgent } }
{
$set: {
title: args.title,
userAgent: args.userAgent,
autoRefresh: args.autoRefresh,
},
}
);
if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Playlist details were not updated');
}
this.sendAllPlaylists(event);
});
ipcMain.on(
PLAYLIST_UPDATE,
(event, args: { id: string; filePath?: string; url?: string }) => {
if (args.filePath && args.id) {
this.fetchPlaylistByFilePath(args.id, args.filePath, event);
} else if (args.url && args.id) {
this.fetchPlaylistByUrl(args.id, args.url, event);
}
}
);
}
/**
@@ -162,6 +190,9 @@ export class Api {
importDate: 1,
userAgent: 1,
filename: 1,
filePath: 1,
autoRefresh: 1,
updateDate: 1,
}
);
event.sender.send('playlist-all-result', {
@@ -204,9 +235,15 @@ export class Api {
* Saves playlist to the localStorage
* @param name name of the playlist
* @param playlist playlist to save
* @param url url of the playlist
* @param urlOrPath absolute fs path or url of the playlist
* @param uploadType upload type - by file or via an url
*/
createPlaylistObject(name: string, playlist: any, url?: string): Playlist {
createPlaylistObject(
name: string,
playlist: any,
urlOrPath?: string,
uploadType?: 'URL' | 'FILE'
): Playlist {
return {
id: guid(),
_id: guid(),
@@ -223,10 +260,131 @@ export class Api {
importDate: new Date().toISOString(),
lastUsage: new Date().toISOString(),
favorites: [],
...(url ? { url } : {}),
autoRefresh: false,
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
};
}
saveUpdatedPlaylist(
id: string,
playlist: any
): Promise<{
numAffected: number;
upsert: boolean;
}> {
return db.update(
{ _id: id },
{
$set: {
playlist,
count: playlist.items.length,
updateDate: Date.now(),
},
}
);
}
/**
* Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer
* @param id id of the playlist to update
* @param playlistString updated playlist as string
* @param event ipc event to send the response back to the renderer
*/
async handlePlaylistRefresh(
id: string,
playlistString: any,
event: Electron.IpcMainEvent
): Promise<void> {
const playlist = this.convertFileStringToPlaylist(playlistString);
const updated = await this.saveUpdatedPlaylist(id, playlist);
if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Playlist details were not updated');
}
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
});
// send all playlists back to the renderer process
this.sendAllPlaylists(event);
}
/**
* Fetches the playlist from the given url and triggers the update operation
* @param id id of the playlist to update
* @param playlistString updated playlist as string
* @param event ipc event to send the response back to the renderer
*/
async fetchPlaylistByUrl(
id: string,
url: string,
event: Electron.IpcMainEvent
): Promise<void> {
try {
await axios
.get(url)
.then((result) =>
this.handlePlaylistRefresh(id, result.data, event)
);
} catch (err) {
event.sender.send(ERROR, {
message: `${err.response.statusText}. Please check the entered playlist URL again.`,
status: err.response.status,
});
}
}
/**
* Fetches the playlist from the given path from the file system and triggers the update operation
* @param id id of the playlist to update
* @param playlistString updated playlist as string
* @param event ipc event to send the response back to the renderer
*/
fetchPlaylistByFilePath(
id: string,
path: string,
event: Electron.IpcMainEvent
): void {
try {
fs.readFile(path, 'utf-8', async (err, data) => {
if (err) {
this.handleFileNotFoundError(err, event);
return;
}
this.handlePlaylistRefresh(id, data, event);
});
} catch (err) {
this.handleFileNotFoundError(err, event);
}
}
/**
* Sends an error message to the renderer process
* @param error
* @param event
*/
handleFileNotFoundError(
error: {
errno: string;
code: string;
syscall: string;
path: string;
},
event: Electron.IpcMainEvent
) {
console.error(error);
event.sender.send(ERROR, {
message: `Sorry, playlist was not found (${error.path})`,
status: 'ENOENT',
});
}
convertFileStringToPlaylist(m3uString: string): any {
return this.parsePlaylist(m3uString.split('\n'));
}
/**
* Parses string based array to playlist object
* @param m3uArray m3u playlist as array with strings
+5
View File
@@ -7,6 +7,11 @@ export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE';
// Playlist related commands
export const PLAYLIST_SAVE_DETAILS = 'PLAYLIST:SAVE_DETAILS';
export const PLAYLIST_PARSE = 'PLAYLIST:PARSE_PLAYLIST';
export const PLAYLIST_PARSE_RESPONSE = 'PLAYLIST:PARSE_PLAYLIST_RESPONSE';
export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE';
export const PLAYLIST_UPDATE_RESPONSE = 'PLAYLIST:UPDATE_RESPONSE';
// General
export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW';
export const ERROR = 'error';
+2680 -86
View File
File diff suppressed because it is too large. Load diff
+5 -1
View File
@@ -96,9 +96,13 @@
"mocha": "7.2.0",
"ng-mocks": "11.5.0",
"npm-run-all": "4.1.5",
"prettier": "2.3.0",
"rxjs": "6.6.3",
"semantic-release": "17.2.3",
"spectron": "12.0.0",
"stylelint": "13.13.1",
"stylelint-config-standard": "22.0.0",
"stylelint-scss": "3.19.0",
"ts-node": "8.10.2",
"tslib": "2.1.0",
"typescript": "4.1.3",
@@ -150,4 +154,4 @@
"lcov"
]
}
}
}
-3
View File
@@ -1,3 +0,0 @@
:host {
}
+2 -4
View File
@@ -28,7 +28,6 @@ new Titlebar({
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
/** Visibility flag of the "what is new" modal dialog */
@@ -157,9 +156,8 @@ export class AppComponent {
this.storage.get('version').subscribe((version) => {
const isNewVersion = semver.gt(actualVersion, version || '0.0.0');
if (!version || isNewVersion) {
this.modals = this.whatsNewService.getModalsByVersion(
actualVersion
);
this.modals =
this.whatsNewService.getModalsByVersion(actualVersion);
this.setDialogVisibility(true);
}
this.setVersion(actualVersion);
@@ -1,8 +1,7 @@
.drop-container {
margin: 10px;
padding: 10px;
height: calc(100vh - 230px);
height: calc(100vh - 235px);
border: 2px dashed #ccc;
border-radius: 3px;
text-align: center;
@@ -36,6 +35,9 @@
display: inline-block;
text-transform: uppercase;
border-radius: 4px;
cursor: pointer;
line-height: 35px;
padding: 3px 10px;
}
.input-file:focus + label,
@@ -43,12 +45,6 @@
background-color: #202b69;
}
.input-file + label {
cursor: pointer;
line-height: 35px;
padding: 3px 10px;
}
.upload-icon {
font-size: 80px;
width: 80px;
+1
View File
@@ -22,6 +22,7 @@
<app-recent-playlists
[playlists]="playlists"
(playlistClicked)="getPlaylist($event)"
(refreshClicked)="refreshPlaylist($event)"
(removeClicked)="removePlaylist($event)"
>
</app-recent-playlists>
+122 -2
View File
@@ -1,7 +1,10 @@
import { UploadFile } from 'ngx-uploader';
import { PLAYLIST_PARSE } from './../../../electron/ipc-commands';
import { PLAYLIST_UPDATE } from './../../../ipc-commands';
import { TranslatePipe } from '@ngx-translate/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MockComponent, MockModule, MockPipe } from 'ng-mocks';
import { HomeComponent } from './home.component';
import { HomeComponent, PlaylistMeta } from './home.component';
import { HeaderComponent } from '../shared/components/header/header.component';
import { RecentPlaylistsComponent } from '../home/recent-playlists/recent-playlists.component';
import { FileUploadComponent } from '../home/file-upload/file-upload.component';
@@ -12,6 +15,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { RouterTestingModule } from '@angular/router/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { ElectronService } from '../services/electron.service';
import { Router } from '@angular/router';
class MatSnackBarStub {
open(): void {}
@@ -34,6 +38,8 @@ export class ElectronServiceStub {
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let electronService: ElectronService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -50,7 +56,7 @@ describe('HomeComponent', () => {
MockModule(MatIconModule),
MockModule(MatProgressBarModule),
MockModule(MatSnackBarModule),
RouterTestingModule,
MockModule(RouterTestingModule),
],
providers: [
{ provide: MatSnackBar, useClass: MatSnackBarStub },
@@ -62,6 +68,8 @@ describe('HomeComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
electronService = TestBed.inject(ElectronService);
router = TestBed.inject(Router);
TestBed.inject(ElectronService);
fixture.detectChanges();
});
@@ -69,4 +77,116 @@ describe('HomeComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should send an event to the main process to remove a playlist', () => {
const playlistId = '12345';
spyOn(electronService.ipcRenderer, 'send');
component.removePlaylist(playlistId);
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
'playlist-remove-by-id',
{ id: playlistId }
);
});
it('should send an event to the main process to refresh a playlist', () => {
const playlistMeta: PlaylistMeta = {
_id: 'iptv1',
filePath: '/home/user/lists/iptv.m3u',
} as PlaylistMeta;
spyOn(electronService.ipcRenderer, 'send');
component.refreshPlaylist(playlistMeta);
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
PLAYLIST_UPDATE,
{ id: playlistMeta._id, filePath: playlistMeta.filePath }
);
});
it('should send an event to the main process to get a playlist', () => {
const playlistId = '6789';
spyOn(electronService.ipcRenderer, 'send');
component.getPlaylist(playlistId);
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
'playlist-by-id',
{
id: playlistId,
}
);
});
it('should send an event to the main process to get a playlist by URL', () => {
const playlistTitle = 'playlist.m3u';
const playlistUrl = 'http://test.com/' + playlistTitle;
spyOn(electronService.ipcRenderer, 'send');
component.sendPlaylistsUrl(playlistUrl);
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
'parse-playlist-by-url',
{
title: playlistTitle,
url: playlistUrl,
}
);
});
it('should send an event to the main process to parse a playlist', () => {
spyOn(electronService.ipcRenderer, 'send');
const title = 'my-list.m3u';
const path = '/home/user/iptv/' + title;
const playlistContent = 'test';
const file: UploadFile = {
nativeFile: { path },
name: title,
} as unknown as UploadFile;
const uploadEvent: Event = {
target: { result: playlistContent },
} as unknown as Event;
component.handlePlaylist({ file, uploadEvent });
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
PLAYLIST_PARSE,
{ title, playlist: [playlistContent], path }
);
});
it('should return the last last segment from an url', () => {
expect(component.getLastUrlSegment('http://example.com')).toEqual(
'example.com'
);
expect(
component.getLastUrlSegment('http://example.com/playlist.m3u')
).toEqual('playlist.m3u');
expect(
component.getLastUrlSegment('http://example.com/playlist.m3u/')
).toEqual('');
});
it('should set IPC event listeners', () => {
spyOn(electronService.ipcRenderer, 'on');
component.setRendererListeners();
expect(electronService.ipcRenderer.on).toHaveBeenCalledTimes(
component.commandsList.length
);
});
it('should send notification on file reject', () => {
spyOn(component, 'showNotification');
component.rejectFile('wrong-file.txt');
expect(component.showNotification).toHaveBeenCalledTimes(1);
});
it('should navigate to the player view', () => {
spyOn(router, 'navigateByUrl');
component.navigateToPlayer();
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
expect(router.navigateByUrl).toHaveBeenCalledWith(
'/iptv',
expect.anything()
);
});
it('should remove all ipc listeners on destroy', () => {
spyOn(electronService.ipcRenderer, 'removeAllListeners');
component.ngOnDestroy();
expect(
electronService.ipcRenderer.removeAllListeners
).toHaveBeenCalledTimes(component.commandsList.length);
});
});
+37 -6
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, NgZone } from '@angular/core';
import { UploadFile } from 'ngx-uploader';
import { ChannelStore, createChannel } from '../state';
@@ -6,11 +5,26 @@ import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Playlist } from './playlist.interface';
import { ElectronService } from '../services/electron.service';
import {
ERROR,
PLAYLIST_PARSE,
PLAYLIST_PARSE_RESPONSE,
PLAYLIST_UPDATE,
PLAYLIST_UPDATE_RESPONSE,
} from './../../../ipc-commands';
/** Type to describe meta data of a playlist */
export type PlaylistMeta = Pick<
Playlist,
'count' | 'title' | 'filename' | '_id' | 'url' | 'importDate' | 'userAgent'
| 'count'
| 'title'
| 'filename'
| '_id'
| 'url'
| 'importDate'
| 'userAgent'
| 'filePath'
| 'updateDate'
>;
@Component({
@@ -26,7 +40,7 @@ export class HomeComponent {
/** IPC Renderer commands list with callbacks */
commandsList = [
{
id: 'parse-response',
id: PLAYLIST_PARSE_RESPONSE,
execute: (response: any) => this.setPlaylist(response.payload),
},
{
@@ -46,14 +60,19 @@ export class HomeComponent {
},
},
{
id: 'error',
id: ERROR,
execute: (response: { message: string; status: number }) => {
this.isLoading = false;
this.showNotification(
`Error: ${response.status} ${response.message}. Please check the entered playlist URL again.`
`Error: ${response.status} ${response.message}.`
);
},
},
{
id: PLAYLIST_UPDATE_RESPONSE,
execute: (response: { message: string }) =>
this.showNotification(response.message),
},
];
/**
@@ -110,9 +129,10 @@ export class HomeComponent {
this.isLoading = true;
const result = (payload.uploadEvent.target as FileReader).result;
const array = (result as string).split('\n');
this.electronService.ipcRenderer.send('parse-playlist', {
this.electronService.ipcRenderer.send(PLAYLIST_PARSE, {
title: payload.file.name,
playlist: array,
path: payload.file.nativeFile.path,
});
}
@@ -166,6 +186,17 @@ export class HomeComponent {
});
}
/**
* Sends an IPC event with the playlist details to the main process to trigger the refresh operation
* @param item playlist to update
*/
refreshPlaylist(item: PlaylistMeta): void {
this.electronService.ipcRenderer.send(PLAYLIST_UPDATE, {
id: item._id,
...(item.url ? { url: item.url } : { filePath: item.filePath }),
});
}
/**
* Requests playlist by id
* @param playlistId playlist id
+13
View File
@@ -1,5 +1,14 @@
import { ID } from '@datorama/akita';
/**
* An interface that describe the possible states of the playlist update/refresh process
*/
export enum PlaylistUpdateState {
UPDATED,
IN_PROGRESS,
NOT_UPDATED,
}
/**
* Describes playlist interface
*/
@@ -15,4 +24,8 @@ export interface Playlist {
count: number;
url?: string;
userAgent?: string;
filePath?: string;
autoRefresh: boolean;
updateDate?: string;
updateState?: PlaylistUpdateState;
}
Whitespace-only changes.
@@ -15,7 +15,6 @@ import { Playlist } from '../../playlist.interface';
@Component({
selector: 'app-playlist-info',
templateUrl: './playlist-info.component.html',
styleUrls: ['./playlist-info.component.scss'],
providers: [DatePipe],
})
export class PlaylistInfoComponent {
@@ -32,7 +32,20 @@
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
</ng-container>
<ng-container *ngIf="item.updateDate">
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
</ng-container>
</div>
<button
*ngIf="item.url || item.filePath"
mat-icon-button
color="accent"
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
[matTooltip]="'HOME.PLAYLISTS.REFRESH' | translate"
>
<mat-icon>sync</mat-icon>
</button>
<button
mat-icon-button
color="accent"
@@ -1,9 +1,11 @@
mat-list-item {
cursor: pointer;
}
.mat-list-base {
padding-top: 0 !important;
}
mat-nav-list {
height: calc(100vh - 192px);
overflow: auto;
@@ -13,4 +15,4 @@ mat-nav-list {
font-size: 12px !important;
color: #666;
margin-top: 2px !important;
}
}
@@ -1,5 +1,4 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Playlist } from '../playlist.interface';
import { MatDialog } from '@angular/material/dialog';
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
import { PlaylistMeta } from './../home.component';
@@ -16,12 +15,12 @@ export class RecentPlaylistsComponent {
/** Emits on playlist selection */
@Output() playlistClicked: EventEmitter<string> = new EventEmitter();
/** Emits on playlist refresh click */
@Output() refreshClicked: EventEmitter<PlaylistMeta> = new EventEmitter();
/** Emits on playlist remove click */
@Output() removeClicked: EventEmitter<string> = new EventEmitter();
/** Emits on playlist rename click */
@Output() renameClicked: EventEmitter<Playlist> = new EventEmitter();
/**
* Creates an instance of the component
* @param dialog angular material dialog reference
@@ -4,4 +4,4 @@ form {
button {
text-transform: uppercase;
}
}
+2
View File
@@ -15,6 +15,7 @@ import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatDialogModule } from '@angular/material/dialog';
import { MatCheckboxModule } from '@angular/material/checkbox';
@NgModule({
exports: [
@@ -34,6 +35,7 @@ import { MatDialogModule } from '@angular/material/dialog';
ScrollingModule,
MatGridListModule,
MatTooltipModule,
MatCheckboxModule,
],
})
export class MaterialModule {}
@@ -2,12 +2,6 @@
background: #ddd;
}
::ng-deep {
.dark-theme .active {
background: #000;
}
}
.mat-list-base {
padding-top: 0;
}
@@ -39,9 +33,14 @@
}
::ng-deep {
.dark-theme .active {
background: #000;
}
.mat-tab-label-content {
text-transform: uppercase;
}
.mat-expansion-panel {
border-radius: 0 !important;
}
@@ -26,4 +26,4 @@
.description-icon {
float: right;
cursor: pointer;
}
}
@@ -1,4 +1,4 @@
#video-player {
width: 100%;
height: calc(100vh - 100px);
}
}
@@ -4,4 +4,4 @@
position: relative !important;
width: 100% !important;
height: calc(100vh - 85px);
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ class MatSnackBarStub {
open(): void {}
}
class MockRouter {
export class MockRouter {
navigateByUrl(url: string) {
return url;
}
@@ -4,11 +4,11 @@
}
.title {
font-size: 1.1em
font-size: 1.1em;
}
.subtitle {
color: #999;
font-size: 0.9em;
margin-top: 10px
}
margin-top: 10px;
}
+4 -2
View File
@@ -58,7 +58,9 @@
"USER_AGENT": "User agent",
"SAVE": "Speichern",
"ORIGINAL_FILENAME": "Original-Dateinamen"
}
},
"UPDATED": "Aktualisiert am",
"REFRESH": "Aktualisieren"
},
"TABS": {
"FILE_UPLOAD": "Aus Dateisystem hochladen",
@@ -102,4 +104,4 @@
"OPEN_EPG_LIST": "TV-Guide (EPG) öffnen",
"TOGGLE_FAVORITE_FLAG": "Als Favoriten hinzufügen/entfernen"
}
}
}
+4 -2
View File
@@ -26,7 +26,9 @@
"SAVE": "Save",
"USER_AGENT": "User agent",
"ORIGINAL_FILENAME": "Original filename"
}
},
"UPDATED": "Updated",
"REFRESH": "Refresh playlist"
},
"FILE_UPLOAD": {
"DRAG_DROP": "Drag & drop files here",
@@ -102,4 +104,4 @@
"RUSSIAN": "Русский",
"GERMAN": "Deutsch"
}
}
}
+4 -2
View File
@@ -26,7 +26,9 @@
"SAVE": "Сохранить",
"USER_AGENT": "User agent",
"ORIGINAL_FILENAME": "Имя файла при загрузке"
}
},
"UPDATED": "Обновлен",
"REFRESH": "Обновить плейлист"
},
"FILE_UPLOAD": {
"DRAG_DROP": "Перетащите файл в эту область",
@@ -102,4 +104,4 @@
"RUSSIAN": "Русский",
"GERMAN": "Deutsch"
}
}
}
+5 -46
View File
@@ -1,66 +1,25 @@
$material-icons-font-path: '~material-icons/iconfont/';
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
@import '~material-icons/iconfont/material-icons.scss';
@import '~@angular/material/theming';
@include mat-core();
@import 'themes/';
@include mat-core();
body,
html {
background: #fff;
font-family: caption;
user-select: none;
}
.mat-toolbar.mat-primary {
background: #000000;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
}
/* CAN (MUST) BE REMOVED ! Sample Global style */
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.title {
color: white;
margin: 0;
padding: 50px 20px;
}
a {
color: #fff !important;
text-transform: uppercase;
text-decoration: none;
background: #ed3330;
padding: 20px;
border-radius: 5px;
display: inline-block;
border: none;
transition: all 0.4s ease 0s;
&:hover {
background: #fff;
color: #ed3330 !important;
letter-spacing: 1px;
-webkit-box-shadow: 0px 5px 40px -10px rgba(0, 0, 0, 0.57);
-moz-box-shadow: 0px 5px 40px -10px rgba(0, 0, 0, 0.57);
box-shadow: 5px 40px -10px rgba(0, 0, 0, 0.57);
transition: all 0.4s ease 0s;
}
}
.mat-toolbar.mat-primary {
background: #000;
}
.full-width {
+16 -15
View File
@@ -1,18 +1,18 @@
$black: (
50: #000000,
100: #000000,
200: #000000,
300: #000000,
400: #000000,
500: #000000,
600: #000000,
700: #000000,
800: #000000,
900: #000000,
A100: #000000,
A200: #000000,
A400: #000000,
A700: #000000,
50: #000,
100: #000,
200: #000,
300: #000,
400: #000,
500: #000,
600: #000,
700: #000,
800: #000,
900: #000,
A100: #000,
A200: #000,
A400: #000,
A700: #000,
contrast: (
50: $light-primary-text,
100: $light-primary-text,
@@ -43,6 +43,7 @@ $angular-default-theme: mat-light-theme(
),
)
);
@include angular-material-theme($angular-default-theme);
@import 'dark';
@include angular-material-theme($angular-default-theme);