Skip to content

Commit

Permalink
Merge pull request #237 from 4gray/feature/export-import-playlists
Browse files Browse the repository at this point in the history
Feature/export import playlists
  • Loading branch information
4gray authored Mar 17, 2023
2 parents 8ea122b + a177f69 commit cd24139
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 10 deletions.
26 changes: 20 additions & 6 deletions src/app/services/playlists.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
}
53 changes: 53 additions & 0 deletions src/app/settings/settings.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
12 changes: 11 additions & 1 deletion src/app/settings/settings.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {}
}
Expand Down Expand Up @@ -80,6 +89,7 @@ describe('SettingsComponent', () => {
},
StorageMap,
provideMockStore(),
MockProviders(NgxIndexedDBService, PlaylistsService),
],
imports: [
HttpClientTestingModule,
Expand Down
89 changes: 88 additions & 1 deletion src/app/settings/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
) {}

Expand Down Expand Up @@ -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()),
});
}
}
4 changes: 4 additions & 0 deletions src/app/state/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,7 @@ export const updatePlaylistPositions = createAction(
positionUpdates: { id: string; changes: { position: number } }[];
}>()
);

export const removeAllPlaylists = createAction(
`${STORE_KEY} Remove all playlists`
);
25 changes: 24 additions & 1 deletion src/app/state/effects.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
) {}
}
6 changes: 6 additions & 0 deletions src/app/state/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ export const playlistReducer = createReducer(
state.playlists
),
};
}),
on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => {
return {
...state,
playlists: playlistsAdapter.removeAll(state.playlists),
};
})
);

Expand Down
15 changes: 14 additions & 1 deletion src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit cd24139

Please sign in to comment.