mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-21 05:43:19 -07:00
feat: Added the features service
This commit is contained in:
parent
9b1a1062ac
commit
689a869507
14 changed files with 197 additions and 6 deletions
24
src/Ombi.Settings/Settings/Models/FeatureSettings.cs
Normal file
24
src/Ombi.Settings/Settings/Models/FeatureSettings.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ombi.Settings.Settings.Models
|
||||||
|
{
|
||||||
|
public class FeatureSettings : Settings
|
||||||
|
{
|
||||||
|
public List<FeatureEnablement> Features { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FeatureEnablement
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FeatureNames
|
||||||
|
{
|
||||||
|
public const string Movie4KRequests = nameof(Movie4KRequests);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
|
|
||||||
namespace Ombi.Store.Context
|
namespace Ombi.Store.Context
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { CustomPageComponent } from "./custompage/custompage.component";
|
||||||
import { CustomizationState } from "./state/customization/customization.state";
|
import { CustomizationState } from "./state/customization/customization.state";
|
||||||
import { DataViewModule } from "primeng/dataview";
|
import { DataViewModule } from "primeng/dataview";
|
||||||
import { DialogModule } from "primeng/dialog";
|
import { DialogModule } from "primeng/dialog";
|
||||||
|
import { FEATURES_INITIALIZER } from "./state/features/features-initializer";
|
||||||
|
import { FeatureState } from "./state/features";
|
||||||
import { JwtModule } from "@auth0/angular-jwt";
|
import { JwtModule } from "@auth0/angular-jwt";
|
||||||
import { LandingPageComponent } from "./landingpage/landingpage.component";
|
import { LandingPageComponent } from "./landingpage/landingpage.component";
|
||||||
import { LandingPageService } from "./services";
|
import { LandingPageService } from "./services";
|
||||||
|
@ -38,6 +40,8 @@ import { MatInputModule } from "@angular/material/input";
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatListModule } from '@angular/material/list';
|
||||||
import { MatMenuModule } from "@angular/material/menu";
|
import { MatMenuModule } from "@angular/material/menu";
|
||||||
import { MatNativeDateModule } from '@angular/material/core';
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
|
import { MatPaginatorI18n } from "./localization/MatPaginatorI18n";
|
||||||
|
import { MatPaginatorIntl } from "@angular/material/paginator";
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
|
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
|
||||||
|
@ -63,11 +67,9 @@ import { StorageService } from "./shared/storage/storage-service";
|
||||||
import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component";
|
import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component";
|
||||||
import { TooltipModule } from "primeng/tooltip";
|
import { TooltipModule } from "primeng/tooltip";
|
||||||
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
||||||
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor";
|
import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor";
|
||||||
import { environment } from "../environments/environment";
|
import { environment } from "../environments/environment";
|
||||||
import { MatPaginatorIntl } from "@angular/material/paginator";
|
|
||||||
import { TranslateService } from "@ngx-translate/core";
|
|
||||||
import { MatPaginatorI18n } from "./localization/MatPaginatorI18n";
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: "*", component: PageNotFoundComponent },
|
{ path: "*", component: PageNotFoundComponent },
|
||||||
|
@ -162,7 +164,7 @@ export function JwtTokenGetter() {
|
||||||
}),
|
}),
|
||||||
SidebarModule,
|
SidebarModule,
|
||||||
MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, LayoutModule, MatSlideToggleModule,
|
MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, LayoutModule, MatSlideToggleModule,
|
||||||
NgxsModule.forRoot([CustomizationState], {
|
NgxsModule.forRoot([CustomizationState, FeatureState], {
|
||||||
developmentMode: !environment.production,
|
developmentMode: !environment.production,
|
||||||
}),
|
}),
|
||||||
...environment.production ? [] :
|
...environment.production ? [] :
|
||||||
|
@ -205,6 +207,7 @@ export function JwtTokenGetter() {
|
||||||
StorageService,
|
StorageService,
|
||||||
RequestService,
|
RequestService,
|
||||||
SignalRNotificationService,
|
SignalRNotificationService,
|
||||||
|
FEATURES_INITIALIZER,
|
||||||
CUSTOMIZATION_INITIALIZER,
|
CUSTOMIZATION_INITIALIZER,
|
||||||
{
|
{
|
||||||
provide: APP_BASE_HREF,
|
provide: APP_BASE_HREF,
|
||||||
|
|
|
@ -146,3 +146,8 @@ export enum INotificationAgent {
|
||||||
Webhook = 9,
|
Webhook = 9,
|
||||||
WhatsApp = 10
|
WhatsApp = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFeatureEnablement {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
19
src/Ombi/ClientApp/src/app/services/feature.service.ts
Normal file
19
src/Ombi/ClientApp/src/app/services/feature.service.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { APP_BASE_HREF } from "@angular/common";
|
||||||
|
import { Injectable, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { IFeatureEnablement } from "../interfaces";
|
||||||
|
import { ServiceHelpers } from "./service.helpers";
|
||||||
|
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class FeatureService extends ServiceHelpers {
|
||||||
|
constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) {
|
||||||
|
super(http, "/api/v2/Features/", href);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFeatures(): Observable<IFeatureEnablement[]> {
|
||||||
|
return this.http.get<IFeatureEnablement[]>(this.url, {headers: this.headers});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import { ICustomizationSettings } from "../../interfaces";
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { SettingsService } from "../../services";
|
import { SettingsService } from "../../services";
|
||||||
import { produce } from 'immer';
|
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from "rxjs/operators";
|
||||||
|
|
||||||
@State({
|
@State({
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { APP_INITIALIZER } from "@angular/core";
|
||||||
|
import { FeaturesFacade } from "./features.facade";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
export const FEATURES_INITIALIZER = {
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: (featureFacade: FeaturesFacade) => (): Observable<unknown> => featureFacade.loadFeatures(),
|
||||||
|
multi: true,
|
||||||
|
deps: [FeaturesFacade],
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class LoadFeatures {
|
||||||
|
public static readonly type = '[Features] LoadAll';
|
||||||
|
}
|
21
src/Ombi/ClientApp/src/app/state/features/features.facade.ts
Normal file
21
src/Ombi/ClientApp/src/app/state/features/features.facade.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { FeaturesSelectors } from "./features.selectors";
|
||||||
|
import { IFeatureEnablement } from "../../interfaces";
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { LoadFeatures } from "./features.actions";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { Store } from "@ngxs/store";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class FeaturesFacade {
|
||||||
|
|
||||||
|
public constructor(private store: Store) {}
|
||||||
|
|
||||||
|
public features$ = (): Observable<IFeatureEnablement[]> => this.store.select(FeaturesSelectors.features);
|
||||||
|
|
||||||
|
public loadFeatures = (): Observable<unknown> => this.store.dispatch(new LoadFeatures());
|
||||||
|
|
||||||
|
public is4kEnabled = (): boolean => this.store.selectSnapshot(FeaturesSelectors.is4kEnabled);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { ICustomizationSettings, IFeatureEnablement } from "../../interfaces";
|
||||||
|
|
||||||
|
import { FEATURES_STATE_TOKEN } from "./types";
|
||||||
|
import { Selector } from "@ngxs/store";
|
||||||
|
|
||||||
|
export class FeaturesSelectors {
|
||||||
|
|
||||||
|
@Selector([FEATURES_STATE_TOKEN])
|
||||||
|
public static features(features: IFeatureEnablement[]): IFeatureEnablement[] {
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Selector([FeaturesSelectors.features])
|
||||||
|
public static is4kEnabled(features: IFeatureEnablement[]): boolean {
|
||||||
|
return features.filter(x => x.name === "Movie4KRequests")[0].enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
26
src/Ombi/ClientApp/src/app/state/features/features.state.ts
Normal file
26
src/Ombi/ClientApp/src/app/state/features/features.state.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Action, State, StateContext } from "@ngxs/store";
|
||||||
|
|
||||||
|
import { FEATURES_STATE_TOKEN } from "./types";
|
||||||
|
import { FeatureService } from "../../services/feature.service";
|
||||||
|
import { IFeatureEnablement } from "../../interfaces";
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { LoadFeatures } from "./features.actions";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { tap } from "rxjs/operators";
|
||||||
|
|
||||||
|
@State({
|
||||||
|
name: FEATURES_STATE_TOKEN
|
||||||
|
})
|
||||||
|
@Injectable()
|
||||||
|
export class FeatureState {
|
||||||
|
constructor(private featuresService: FeatureService) { }
|
||||||
|
|
||||||
|
@Action(LoadFeatures)
|
||||||
|
public load({ setState }: StateContext<IFeatureEnablement[]>): Observable<IFeatureEnablement[]> {
|
||||||
|
return this.featuresService.getFeatures().pipe(
|
||||||
|
tap(features =>
|
||||||
|
setState(features)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
4
src/Ombi/ClientApp/src/app/state/features/index.ts
Normal file
4
src/Ombi/ClientApp/src/app/state/features/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './features.state';
|
||||||
|
export * from './features.actions';
|
||||||
|
export * from './features.facade';
|
||||||
|
export * from './features.selectors';
|
4
src/Ombi/ClientApp/src/app/state/features/types.ts
Normal file
4
src/Ombi/ClientApp/src/app/state/features/types.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { IFeatureEnablement } from "../../interfaces";
|
||||||
|
import { StateToken } from "@ngxs/store";
|
||||||
|
|
||||||
|
export const FEATURES_STATE_TOKEN = new StateToken<IFeatureEnablement[]>('featureEnablement');
|
54
src/Ombi/Controllers/V2/FeaturesController.cs
Normal file
54
src/Ombi/Controllers/V2/FeaturesController.cs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Ombi.Core.Settings;
|
||||||
|
using Ombi.Settings.Settings.Models;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ombi.Controllers.V2
|
||||||
|
{
|
||||||
|
public class FeaturesController : V2Controller
|
||||||
|
{
|
||||||
|
private readonly ISettingsService<FeatureSettings> _features;
|
||||||
|
|
||||||
|
public FeaturesController(ISettingsService<FeatureSettings> features) => _features = features;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<List<FeatureEnablement>> GetFeatures()
|
||||||
|
{
|
||||||
|
var features = await _features.GetSettingsAsync();
|
||||||
|
return PopulateFeatures(features?.Features ?? new List<FeatureEnablement>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FeatureEnablement> PopulateFeatures(List<FeatureEnablement> existingFeatures)
|
||||||
|
{
|
||||||
|
var supported = GetSupportedFeatures().ToList();
|
||||||
|
if (supported.Count == existingFeatures.Count)
|
||||||
|
{
|
||||||
|
return existingFeatures;
|
||||||
|
}
|
||||||
|
var diff = supported.Except(existingFeatures.Select(x => x.Name));
|
||||||
|
|
||||||
|
foreach (var feature in diff)
|
||||||
|
{
|
||||||
|
existingFeatures.Add(new FeatureEnablement
|
||||||
|
{
|
||||||
|
Name = feature
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return existingFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetSupportedFeatures()
|
||||||
|
{
|
||||||
|
FieldInfo[] fieldInfos = typeof(FeatureNames).GetFields(BindingFlags.Public |
|
||||||
|
BindingFlags.Static | BindingFlags.FlattenHierarchy);
|
||||||
|
|
||||||
|
return fieldInfos.Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(string)).Select(x => (string)x.GetValue(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue