mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
11 files changed
+285
-12
No files matched your search
@@ -7,6 +7,8 @@ on:
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -22,12 +24,13 @@ jobs:
|
||||
node-version: '18.10'
|
||||
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
uses: samuelmeuli/action-snapcraft@v2
|
||||
# Only install Snapcraft on Ubuntu
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
with:
|
||||
# Log in to Snap Store
|
||||
snapcraft_token: ${{ secrets.snapcraft_token }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }}
|
||||
|
||||
- name: Package Linux
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
|
||||
@@ -1,3 +1,63 @@
|
||||
# [0.12.0](https://github.com/4gray/iptvnator/compare/v0.11.1...v0.12.0) (2023-03-11)
|
||||
|
||||
|
||||
### Important change
|
||||
|
||||
The storage location of playlists has been changed, now everything is stored in IndexedDB. So after the update you will see a panel offering to migrate playlists.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* macos related window management improvements ([b336680](https://github.com/4gray/iptvnator/commit/**b336680ec93b6c2a78af08bf1847f6e133895719**))
|
||||
* set epgSource as not required field ([049ed6b](https://github.com/4gray/iptvnator/commit/049ed6be519df602fd8eb5071fb17efe1a850000)), closes [#175](https://github.com/4gray/iptvnator/issues/175)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add cmd+q hotkey to close the app (macOS) ([f3e00e7](https://github.com/4gray/iptvnator/commit/f3e00e78aa65b64d058c27b616b0d11d1a374015)), closes [#181](https://github.com/4gray/iptvnator/issues/181)
|
||||
* add dockerfile and docker-compose ([4b97e3d](https://github.com/4gray/iptvnator/commit/4b97e3d4b3b84a57c5c09cc25c0c362de341e2ba))
|
||||
* add italian language ([3e3f18c](https://github.com/4gray/iptvnator/commit/3e3f18cabf3784bcfee17e54771ca0a5dbcbbf33))
|
||||
* draggable channels in favorites list ([ba41c8d](https://github.com/4gray/iptvnator/commit/ba41c8dae5e82bd1f39fb9a6cd8518e25dcdb894))
|
||||
* export playlist as m3u ([7e4d6b1](https://github.com/4gray/iptvnator/commit/7e4d6b171fa87ffb7084344a10d653dd2cb30ea2))
|
||||
* persist window size ([2ce60e0](https://github.com/4gray/iptvnator/commit/2ce60e0a205dd8034626f35bfea1632fcac56529)), closes [#205](https://github.com/4gray/iptvnator/issues/205)
|
||||
* **pwa:** load a m3u playlist as a URL parameter [#176](https://github.com/4gray/iptvnator/issues/176) ([344bd75](https://github.com/4gray/iptvnator/commit/344bd75c876ff3d26e5721e64e7b35cf7547950a))
|
||||
|
||||
|
||||
|
||||
## [0.11.1](https://github.com/4gray/iptvnator/compare/v0.11.0...v0.11.1) (2022-10-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow file:// protocol for epg source ([c4e1076](https://github.com/4gray/iptvnator/commit/c4e107681e55ac168a86b57cf0c6bfd5a2b35c5c))
|
||||
* disable service worker in electron app ([df4e99a](https://github.com/4gray/iptvnator/commit/df4e99a4ae7186322eddcab8b6a81a04bd4c2e1a))
|
||||
* red screen in PIP window ([9756a76](https://github.com/4gray/iptvnator/commit/9756a76a90c05c008cd84c690c82b04c7ec56c87))
|
||||
* service worker injection ([ba3dd88](https://github.com/4gray/iptvnator/commit/ba3dd88dde1129ac21ec6056b81f7ff8b569f5d6))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "Update release.yml" ([5ac1992](https://github.com/4gray/iptvnator/commit/5ac199281d0fa24998cf01dff148316f7684e46a))
|
||||
|
||||
|
||||
|
||||
# [0.11.0](https://github.com/4gray/iptvnator/compare/v0.10.0...v0.11.0) (2022-09-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set default value for video player ([ab1d0bf](https://github.com/4gray/iptvnator/commit/ab1d0bfec85dd8a43606c02a215c6654b809bb15))
|
||||
* show notification after playlist refresh (PWA) ([b18e537](https://github.com/4gray/iptvnator/commit/b18e537ccb25c49002577a7aedd810c81eb981ea))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* check for available updates in PWA ([17b265f](https://github.com/4gray/iptvnator/commit/17b265f5a6bc4cae5c66532ccda3af580b52779c))
|
||||
* implement multi epg view ([b4db751](https://github.com/4gray/iptvnator/commit/b4db751fb66353700dbc9e2285232da28ee655cb))
|
||||
* import playlist as text ([6676fa0](https://github.com/4gray/iptvnator/commit/6676fa0a4267a5cb56697b47f24954581873f3e1))
|
||||
* support multiple epg sources ([f8c6874](https://github.com/4gray/iptvnator/commit/f8c6874ad3734ef74e5de25e83626c7e8e77c55a))
|
||||
|
||||
|
||||
|
||||
# [0.10.0](https://github.com/4gray/iptvnator/compare/v0.9.0...v0.10.0) (2022-04-24)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"version": "0.11.1",
|
||||
"version": "0.12.0",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
"author": {
|
||||
|
||||
@@ -7,12 +7,12 @@ import { Channel } from '../../../shared/channel.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState
|
||||
PlaylistUpdateState,
|
||||
} from '../../../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
createPlaylistObject
|
||||
createPlaylistObject,
|
||||
} from '../../../shared/playlist.utils';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
@@ -193,9 +193,23 @@ export class PlaylistsService {
|
||||
}
|
||||
|
||||
getRawPlaylistById(id: string) {
|
||||
return this.dbService.getByID<Playlist>(DbStores.Playlists, id).pipe(map(playlist => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
return playlist.playlist.header.raw + '\n' + playlist.playlist.items.map(item => item.raw).join('\n');
|
||||
}));
|
||||
return this.dbService.getByID<Playlist>(DbStores.Playlists, id).pipe(
|
||||
map((playlist) => {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
playlist.playlist.header.raw +
|
||||
'\n' +
|
||||
playlist.playlist.items.map((item) => item.raw).join('\n')
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getAllData() {
|
||||
return this.dbService.getAll<Playlist>(DbStores.Playlists);
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
return this.dbService.clear(DbStores.Playlists);
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,59 @@
|
||||
{{ updateMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.IMPORT_EXPORT_DATA' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.IMPORT_EXPORT_DATA_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
mat-button
|
||||
(click)="
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
importData()
|
||||
"
|
||||
>
|
||||
{{ 'SETTINGS.IMPORT_DATA' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
(click)="
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
exportData()
|
||||
"
|
||||
>
|
||||
{{ 'SETTINGS.EXPORT_DATA' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOVE_ALL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
mat-button
|
||||
(click)="
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
removeAll()
|
||||
"
|
||||
color="warn"
|
||||
>
|
||||
{{ 'SETTINGS.REMOVE_ALL_BUTTON' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" *ngIf="!isElectron">
|
||||
{{ 'SETTINGS.EPG_NOTE' | translate }}
|
||||
|
||||
@@ -23,7 +23,13 @@ import {
|
||||
TranslatePipe,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import {
|
||||
MockComponent,
|
||||
MockModule,
|
||||
MockPipe,
|
||||
MockProvider,
|
||||
MockProviders,
|
||||
} from 'ng-mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { EPG_FORCE_FETCH } from '../../../shared/ipc-commands';
|
||||
import { DataService } from '../services/data.service';
|
||||
@@ -35,6 +41,9 @@ import { SettingsComponent } from './settings.component';
|
||||
import { VideoPlayer } from './settings.interface';
|
||||
import { Theme } from './theme.enum';
|
||||
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
}
|
||||
@@ -80,6 +89,7 @@ describe('SettingsComponent', () => {
|
||||
},
|
||||
StorageMap,
|
||||
provideMockStore(),
|
||||
MockProviders(NgxIndexedDBService, PlaylistsService),
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
|
||||
@@ -13,10 +13,14 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { take } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import { EPG_FORCE_FETCH } from '../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { EpgService } from '../services/epg.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { selectIsEpgAvailable } from '../state/selectors';
|
||||
import { SettingsService } from './../services/settings.service';
|
||||
import { Language } from './language.enum';
|
||||
@@ -77,13 +81,15 @@ export class SettingsComponent implements OnInit {
|
||||
* required dependencies into the component
|
||||
*/
|
||||
constructor(
|
||||
private store: Store,
|
||||
private dialogService: DialogService,
|
||||
private electronService: DataService,
|
||||
private epgService: EpgService,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private playlistsService: PlaylistsService,
|
||||
private router: Router,
|
||||
private settingsService: SettingsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private store: Store,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
@@ -265,4 +271,85 @@ export class SettingsComponent implements OnInit {
|
||||
this.epgUrl.removeAt(index);
|
||||
this.settingsForm.markAsDirty();
|
||||
}
|
||||
|
||||
exportData() {
|
||||
this.playlistsService
|
||||
.getAllData()
|
||||
.pipe(take(1))
|
||||
.subscribe((data) => {
|
||||
const blob = new Blob([JSON.stringify(data)], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'playlists.json';
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
importData() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/json';
|
||||
|
||||
input.addEventListener('change', (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const contents = reader.result;
|
||||
|
||||
try {
|
||||
const parsedPlaylists: Playlist[] = JSON.parse(
|
||||
contents.toString()
|
||||
);
|
||||
|
||||
if (!Array.isArray(parsedPlaylists)) {
|
||||
this.snackBar.open(
|
||||
this.translate.instant(
|
||||
'SETTINGS.IMPORT_ERROR'
|
||||
),
|
||||
null,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.addManyPlaylists({
|
||||
playlists: parsedPlaylists,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.snackBar.open(
|
||||
this.translate.instant('SETTINGS.IMPORT_ERROR'),
|
||||
null,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
this.dialogService.openConfirmDialog({
|
||||
title: this.translate.instant('SETTINGS.REMOVE_DIALOG.TITLE'),
|
||||
message: this.translate.instant('SETTINGS.REMOVE_DIALOG.MESSAGE'),
|
||||
onConfirm: (): void =>
|
||||
this.store.dispatch(PlaylistActions.removeAllPlaylists()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -115,3 +115,7 @@ export const updatePlaylistPositions = createAction(
|
||||
positionUpdates: { id: string; changes: { position: number } }[];
|
||||
}>()
|
||||
);
|
||||
|
||||
export const removeAllPlaylists = createAction(
|
||||
`${STORE_KEY} Remove all playlists`
|
||||
);
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatestWith, map, switchMap, tap } from 'rxjs/operators';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
@@ -203,11 +205,32 @@ export class PlaylistEffects {
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
removeAll$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.removeAllPlaylists),
|
||||
switchMap(() => this.playlistsService.removeAll()),
|
||||
tap(() => {
|
||||
this.snackBar.open(
|
||||
this.translate.instant('SETTINGS.PLAYLISTS_REMOVED'),
|
||||
null,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private playlistsService: PlaylistsService,
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private store: Store,
|
||||
private router: Router
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
}
|
||||
@@ -205,6 +205,12 @@ export const playlistReducer = createReducer(
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.removeAll(state.playlists),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
+14
-1
@@ -89,7 +89,20 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Show or hide subtitles by default",
|
||||
"VERSION_DESCRIPTION": "Current version of the application",
|
||||
"EPG_NOTE": "Currently EPG is available only in the electron-based version of IPTVnator.",
|
||||
"EPG_NOTE_URL_TEXT": "Check it here"
|
||||
"EPG_NOTE_URL_TEXT": "Check it here",
|
||||
"IMPORT_EXPORT_DATA": "Import/export playlists",
|
||||
"IMPORT_EXPORT_DATA_DESCRIPTION": "Export or import playlists in the JSON format used within the application.",
|
||||
"IMPORT_DATA": "Import",
|
||||
"EXPORT_DATA": "Export",
|
||||
"REMOVE_ALL": "Remove playlist",
|
||||
"REMOVE_ALL_BUTTON": "Remove",
|
||||
"REMOVE_ALL_DESCRIPTION": "It will remove all existing playlists from the application.",
|
||||
"REMOVE_DIALOG": {
|
||||
"TITLE": "Remove all playlists",
|
||||
"MESSAGE": "Are you sure you want to delete all playlist from the application?"
|
||||
},
|
||||
"IMPORT_ERROR": "Import error, please try with another file.",
|
||||
"PLAYLISTS_REMOVED": "All playlists were removed."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Dark theme",
|
||||
|
||||
Reference in new issue
Block a user