diff --git a/src/app/home/xtream-code-import/xtream-code-import.component.html b/src/app/home/xtream-code-import/xtream-code-import.component.html
index d58f59408..e08c7311b 100644
--- a/src/app/home/xtream-code-import/xtream-code-import.component.html
+++ b/src/app/home/xtream-code-import/xtream-code-import.component.html
@@ -33,12 +33,32 @@
}}
-
+
+
+
+
+
+
+ @if (connectionStatus){
+
+
+ {{ getStatusIcon() }}
+
+ {{ getStatusMessage() }}
+
+ }
diff --git a/src/app/home/xtream-code-import/xtream-code-import.component.ts b/src/app/home/xtream-code-import/xtream-code-import.component.ts
index cba30c835..599f05194 100644
--- a/src/app/home/xtream-code-import/xtream-code-import.component.ts
+++ b/src/app/home/xtream-code-import/xtream-code-import.component.ts
@@ -6,14 +6,18 @@ import {
ReactiveFormsModule,
Validators,
} from '@angular/forms';
-import { MatButtonModule } from '@angular/material/button';
+import { MatButton } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { v4 as uuid } from 'uuid';
import { Playlist } from '../../../../shared/playlist.interface';
-import { DataService } from '../../services/data.service';
+import {
+ PortalStatus,
+ PortalStatusService,
+} from '../../services/portal-status.service';
import { addPlaylist } from '../../state/actions';
@Component({
@@ -22,8 +26,9 @@ import { addPlaylist } from '../../state/actions';
FormsModule,
ReactiveFormsModule,
MatFormFieldModule,
+ MatIcon,
MatInputModule,
- MatButtonModule,
+ MatButton,
TranslateModule,
],
selector: 'app-xtream-code-import',
@@ -39,6 +44,34 @@ import { addPlaylist } from '../../state/actions';
form {
width: 100%;
}
+
+ .status-active {
+ color: #4caf50;
+ }
+
+ .status-inactive {
+ color: #f44336;
+ }
+
+ .status-expired {
+ color: #ff9800;
+ }
+
+ .status-unavailable {
+ color: #9e9e9e;
+ }
+
+ .connection-status {
+ margin: 10px 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .button-row {
+ display: flex;
+ justify-content: space-between;
+ }
`,
],
})
@@ -58,8 +91,46 @@ export class XtreamCodeImportComponent {
importDate: new FormControl(new Date().toISOString()),
});
- dataService = inject(DataService);
- store = inject(Store);
+ readonly store = inject(Store);
+ readonly portalStatusService = inject(PortalStatusService);
+
+ connectionStatus: PortalStatus | null = null;
+ isTestingConnection = false;
+
+ async testConnection(): Promise {
+ if (!this.form.valid) return;
+
+ this.isTestingConnection = true;
+ const serverUrlAsString = this.form.value.serverUrl as string;
+ const url = new URL(serverUrlAsString);
+ const serverUrl = `${url.protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`;
+
+ try {
+ this.connectionStatus =
+ await this.portalStatusService.checkPortalStatus(
+ serverUrl,
+ this.form.value.username as string,
+ this.form.value.password as string
+ );
+ } catch (error) {
+ console.error('Error testing connection:', error);
+ this.connectionStatus = 'unavailable';
+ } finally {
+ this.isTestingConnection = false;
+ }
+ }
+
+ getStatusMessage(): string {
+ return this.portalStatusService.getStatusMessage(this.connectionStatus);
+ }
+
+ getStatusClass(): string {
+ return this.portalStatusService.getStatusClass(this.connectionStatus);
+ }
+
+ getStatusIcon(): string {
+ return this.portalStatusService.getStatusIcon(this.connectionStatus);
+ }
addPlaylist() {
const serverUrlAsString = this.form.value.serverUrl as string;
diff --git a/src/app/services/portal-status.service.ts b/src/app/services/portal-status.service.ts
new file mode 100644
index 000000000..20c33f0bd
--- /dev/null
+++ b/src/app/services/portal-status.service.ts
@@ -0,0 +1,110 @@
+import { Injectable, inject } from '@angular/core';
+import { XTREAM_REQUEST } from '../../../shared/ipc-commands';
+import { DataService } from './data.service';
+
+export type PortalStatus = 'active' | 'inactive' | 'expired' | 'unavailable';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class PortalStatusService {
+ private readonly dataService = inject(DataService);
+
+ /**
+ * Checks the status of an Xtream Code portal
+ *
+ * @param serverUrl The base URL of the server
+ * @param username The username for authentication
+ * @param password The password for authentication
+ * @returns A promise that resolves to the portal status
+ */
+ async checkPortalStatus(
+ serverUrl: string,
+ username: string,
+ password: string
+ ): Promise {
+ try {
+ let normalizedUrl = serverUrl;
+ if (serverUrl && !serverUrl.endsWith('/')) {
+ normalizedUrl = serverUrl;
+ }
+
+ let response = await this.dataService.sendIpcEvent(XTREAM_REQUEST, {
+ url: normalizedUrl,
+ params: {
+ password,
+ username,
+ action: 'get_account_info',
+ },
+ });
+ response = response?.payload;
+
+ if (!response?.user_info?.status) {
+ return 'unavailable';
+ }
+
+ if (response.user_info.status === 'Active') {
+ const expDate = new Date(
+ parseInt(response.user_info.exp_date) * 1000
+ );
+ return expDate < new Date() ? 'expired' : 'active';
+ } else {
+ return 'inactive';
+ }
+ } catch (error) {
+ console.error('Error checking portal status:', error);
+ return 'unavailable';
+ }
+ }
+
+ /**
+ * Gets a user-friendly message based on the portal status
+ *
+ * @param status The portal status
+ * @returns A message describing the status
+ */
+ getStatusMessage(status: PortalStatus | null): string {
+ switch (status) {
+ case 'active':
+ return 'Connection successful! Portal is active.';
+ case 'inactive':
+ return 'Portal is inactive.';
+ case 'expired':
+ return 'Portal subscription has expired.';
+ case 'unavailable':
+ return 'Could not connect to the portal.';
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Gets a CSS class name based on the portal status
+ *
+ * @param status The portal status
+ * @returns A CSS class name
+ */
+ getStatusClass(status: PortalStatus | null): string {
+ return status ? `status-${status}` : '';
+ }
+
+ /**
+ * Gets an icon name based on the portal status
+ *
+ * @param status The portal status
+ * @returns A material icon name
+ */
+ getStatusIcon(status: PortalStatus | null): string {
+ switch (status) {
+ case 'active':
+ return 'check_circle';
+ case 'inactive':
+ return 'cancel';
+ case 'expired':
+ return 'warning';
+ case 'unavailable':
+ default:
+ return 'error';
+ }
+ }
+}
diff --git a/src/app/services/pwa.service.ts b/src/app/services/pwa.service.ts
index 77bfe1bb3..da42d88d9 100644
--- a/src/app/services/pwa.service.ts
+++ b/src/app/services/pwa.service.ts
@@ -5,7 +5,7 @@ import { Params } from '@angular/router';
import { SwUpdate } from '@angular/service-worker';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
-import { catchError, EMPTY, throwError } from 'rxjs';
+import { catchError, firstValueFrom, throwError } from 'rxjs';
import {
ERROR,
PLAYLIST_PARSE_BY_URL,
@@ -63,13 +63,13 @@ export class PwaService extends DataService {
* @param type ipc command type
* @param payload payload
*/
- sendIpcEvent(type: string, payload?: unknown): void {
+ sendIpcEvent(type: string, payload?: unknown) {
if (type === PLAYLIST_PARSE_BY_URL) {
this.fetchFromUrl(payload);
} else if (type === PLAYLIST_UPDATE) {
this.refreshPlaylist(payload);
} else if (type === XTREAM_REQUEST) {
- this.forwardXtreamRequest(
+ return this.forwardXtreamRequest(
payload as { url: string; params: Record }
);
} else if (type === STALKER_REQUEST) {
@@ -151,7 +151,7 @@ export class PwaService extends DataService {
return message;
}
- forwardXtreamRequest(payload: {
+ async forwardXtreamRequest(payload: {
url: string;
params: Record;
macAddress?: string;
@@ -163,39 +163,44 @@ export class PwaService extends DataService {
},
}
: {};
- return this.http
- .get(`${this.corsProxyUrl}/xtream`, {
- params: {
- url: payload.url,
- ...payload.params,
- },
- ...headers,
- })
- .pipe(
- catchError((response) => {
- window.postMessage({
- type: ERROR,
- status: response.error.status,
- message: response.error.message ?? 'Unknown error',
- });
- return EMPTY;
+ try {
+ let result: any;
+ const response = await firstValueFrom(
+ this.http.get(`${this.corsProxyUrl}/xtream`, {
+ params: {
+ url: payload.url,
+ ...payload.params,
+ },
+ ...headers,
})
- )
- .subscribe((response) => {
- if (!(response as any).payload) {
- window.postMessage({
- type: ERROR,
- status: (response as any).status,
- message: (response as any).message ?? 'Unknown error',
- });
- } else {
- window.postMessage({
- type: XTREAM_RESPONSE,
- payload: (response as any).payload,
- action: payload.params.action,
- });
- }
+ );
+
+ if (!(response as any).payload) {
+ if (payload.params.action === 'get_account_info') return;
+
+ result = {
+ type: ERROR,
+ status: (response as any).status,
+ message: (response as any).message ?? 'Unknown error',
+ };
+ window.postMessage(result);
+ } else {
+ result = {
+ type: XTREAM_RESPONSE,
+ payload: (response as any).payload,
+ action: payload.params.action,
+ };
+ window.postMessage(result);
+ }
+ return result;
+ } catch (error: any) {
+ if (payload.params.action === 'get_account_info') return;
+ window.postMessage({
+ type: ERROR,
+ status: error.error?.status,
+ message: error.error?.message ?? 'Unknown error',
});
+ }
}
forwardStalkerRequest(payload: {
diff --git a/src/app/services/tauri.service.ts b/src/app/services/tauri.service.ts
index 19fe9368b..af9107629 100644
--- a/src/app/services/tauri.service.ts
+++ b/src/app/services/tauri.service.ts
@@ -34,7 +34,7 @@ export class TauriService extends DataService {
} else if (type === 'PLAYLIST_UPDATE') {
console.log('PLAYLIST_UPDATE');
} else if (type === 'XTREAM_REQUEST') {
- this.forwardXtreamRequest(
+ return await this.forwardXtreamRequest(
payload as { url: string; params: Record }
);
} else if (type === 'STALKER_REQUEST') {
@@ -174,6 +174,7 @@ export class TauriService extends DataService {
url: string;
params: Record;
}) {
+ let result: any;
const url = new URL(`${payload.url}/player_api.php`);
Object.entries(payload.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
@@ -183,18 +184,21 @@ export class TauriService extends DataService {
const responseBody = await response.json();
if (!responseBody) {
- window.postMessage({
+ result = {
type: ERROR,
status: response.status,
message: responseBody.message ?? 'Unknown error',
- });
+ };
+ window.postMessage(result);
} else {
- window.postMessage({
+ result = {
type: XTREAM_RESPONSE,
payload: responseBody,
action: payload.params.action,
- });
+ };
+ window.postMessage(result);
}
+ return result;
}
removeAllListeners(type: string): void {