rework the pages

This commit is contained in:
tidusjar 2025-05-12 22:19:59 +01:00
parent 6344ae98cd
commit cb6d441ccd
5 changed files with 565 additions and 180 deletions

View file

@ -5,20 +5,6 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment=""> <list default="true" id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/contentModel.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/modules.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/riderModule.iml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.vscode/tasks.json" beforeDir="false" afterPath="$PROJECT_DIR$/.vscode/tasks.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Helpers/NotificationType.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Helpers/NotificationType.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Schedule.Tests/Properties/launchSettings.json" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Settings/Settings/Models/External/PlexSettings.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Settings/Settings/Models/External/PlexSettings.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Store/Context/OmbiContext.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Store/Context/OmbiContext.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/interfaces/ISettings.ts" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/interfaces/ISettings.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/plex.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/plex.component.html" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/plex.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/plex.component.html" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
@ -416,7 +402,7 @@
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"fb34c741-04ca-4b4f-8ea1-651a011b42c8.executor": "Debug", "fb34c741-04ca-4b4f-8ea1-651a011b42c8.executor": "Debug",
"git-widget-placeholder": "develop", "git-widget-placeholder": "watchlist-expired-notification",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)",
@ -512,7 +498,7 @@
<workItem from="1563957162999" duration="5401000" /> <workItem from="1563957162999" duration="5401000" />
<workItem from="1745681294313" duration="1814000" /> <workItem from="1745681294313" duration="1814000" />
<workItem from="1747080279165" duration="838000" /> <workItem from="1747080279165" duration="838000" />
<workItem from="1747082180432" duration="1399000" /> <workItem from="1747082180432" duration="1994000" />
</task> </task>
<servers /> <servers />
</component> </component>
@ -649,6 +635,19 @@
</properties> </properties>
<option name="timeStamp" value="11" /> <option name="timeStamp" value="11" />
</line-breakpoint> </line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>100</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="4602" />
</startOffsets>
<endOffsets>
<option value="4636" />
</endOffsets>
</properties>
<option name="timeStamp" value="12" />
</line-breakpoint>
</breakpoints> </breakpoints>
</breakpoint-manager> </breakpoint-manager>
<watches-manager> <watches-manager>

View file

@ -1,38 +1,40 @@
<div class="small-middle-container"> <div class="watchlist-dialog-container">
<fieldset style="fieldset"> <mat-card class="watchlist-dialog-card">
<legend mat-dialog-title>Watchlist User Errors</legend> <mat-card-header>
<div mat-dialog-content> <mat-card-title>Watchlist User Errors</mat-card-title>
<p> </mat-card-header>
If there is an authentication error, this is because of an authentication issue with Plex (Token has expired). <mat-card-content>
If this happens the user needs to re-login to Ombi. <div class="watchlist-info-section">
</p> <mat-icon color="warn" class="info-icon">error_outline</mat-icon>
<p> <span>
<em class="fa-solid fa-check key"></em> Successfully syncing the watchlist If there is an authentication error, this is because of an authentication issue with Plex (Token has expired).
<br> If this happens the user needs to re-login to Ombi.
<em class="fa-solid fa-times key"></em> Authentication error syncing the watchlist </span>
<br> </div>
<em class="fas fa-user-alt-slash key"></em> Not enabled for user (They need to log into Ombi via Plex) <div class="watchlist-legend">
</p> <span><mat-icon color="primary">check_circle</mat-icon> Successfully syncing the watchlist</span>
<table mat-table *ngIf="dataSource" [dataSource]="dataSource" matSort class="mat-elevation-z8"> <span><mat-icon color="warn">cancel</mat-icon> Authentication error syncing the watchlist</span>
<ng-container matColumnDef="userName"> <span><mat-icon color="accent">person_off</mat-icon> Not enabled for user (They need to log into Ombi via Plex)</span>
<th mat-header-cell *matHeaderCellDef> Username </th> </div>
<td mat-cell *matCellDef="let element"> {{element.userName}} </td> <table mat-table *ngIf="dataSource" [dataSource]="dataSource" matSort class="mat-elevation-z8 modern-table">
</ng-container> <ng-container matColumnDef="userName">
<ng-container matColumnDef="syncStatus"> <th mat-header-cell *matHeaderCellDef> Username </th>
<th mat-header-cell *matHeaderCellDef> Watchlist Sync Result </th> <td mat-cell *matCellDef="let element"> {{element.userName}} </td>
<td mat-cell *matCellDef="let element"> </ng-container>
<em *ngIf="element.syncStatus === WatchlistSyncStatus.Successful" class="fa-solid fa-check"></em> <ng-container matColumnDef="syncStatus">
<em *ngIf="element.syncStatus === WatchlistSyncStatus.Failed" class="fa-solid fa-times"></em> <th mat-header-cell *matHeaderCellDef> Watchlist Sync Result </th>
<em *ngIf="element.syncStatus === WatchlistSyncStatus.NotEnabled" class="fas fa-user-alt-slash"></em> <td mat-cell *matCellDef="let element">
</td> <mat-icon *ngIf="element.syncStatus === WatchlistSyncStatus.Successful" color="primary">check_circle</mat-icon>
</ng-container> <mat-icon *ngIf="element.syncStatus === WatchlistSyncStatus.Failed" color="warn">cancel</mat-icon>
<mat-icon *ngIf="element.syncStatus === WatchlistSyncStatus.NotEnabled" color="accent">person_off</mat-icon>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> </td>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </ng-container>
</table> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
</div> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<mat-dialog-actions align="end"> </table>
<button mat-button mat-dialog-close>Close</button> </mat-card-content>
</mat-dialog-actions> <mat-card-actions align="end">
</fieldset> <button mat-stroked-button mat-dialog-close color="accent">Close</button>
</mat-card-actions>
</mat-card>
</div> </div>

View file

@ -10,4 +10,96 @@
.key { .key {
width: 40px; width: 40px;
}
.watchlist-dialog-container {
display: flex;
justify-content: center;
align-items: flex-start;
margin: 0 auto;
}
.watchlist-dialog-card {
background: #23272f;
color: #f1f3f6;
border-radius: 12px;
border: 1px solid #353a45;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.25);
min-width: 420px;
max-width: 600px;
width: 100%;
max-height: 70vh;
display: flex;
flex-direction: column;
}
mat-card-content {
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
}
mat-card-header {
border-bottom: 1px solid #353a45;
margin-bottom: 12px;
flex: 0 0 auto;
}
mat-card-title {
color: #fff !important;
font-weight: 700 !important;
letter-spacing: 0.5px;
}
.watchlist-info-section {
display: flex;
align-items: center;
background: #23272f;
color: #e0e3ea;
padding: 12px 0 8px 0;
font-size: 15px;
gap: 12px;
}
.info-icon {
font-size: 28px;
color: #ffb300;
}
.watchlist-legend {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 18px;
margin-top: 8px;
font-size: 14px;
color: #b0b6c3;
}
.watchlist-legend mat-icon {
vertical-align: middle;
margin-right: 6px;
}
.modern-table {
background: transparent;
color: #f1f3f6;
border-radius: 8px;
margin-top: 10px;
}
.modern-table th, .modern-table td {
color: #f1f3f6;
font-size: 15px;
}
mat-card-actions {
padding-top: 16px;
flex: 0 0 auto;
background: #23272f;
}
button[mat-stroked-button] {
font-weight: 600;
border-radius: 6px;
} }

View file

@ -1,141 +1,179 @@
<settings-menu></settings-menu> <settings-menu></settings-menu>
<div class="small-middle-container" *ngIf="settings">
<fieldset style="width:100%;"> <div class="plex-settings-container" *ngIf="settings">
<legend>Plex Configuration</legend> <mat-card class="settings-card">
<div class="col-12"> <mat-card-header>
<div class="md-form-field align-right"> <mat-card-title>Plex Configuration</mat-card-title>
<button (click)="openWatchlistUserLog()" type="button" class="mat-focus-indicator mat-flat-button mat-button-base mat-accent">Watchlist User Errors</button> </mat-card-header>
<mat-card-content>
<!-- Watchlist Settings Section -->
<div class="settings-section">
<div class="section-header">
<h2>Watchlist Settings</h2>
</div>
<div class="settings-grid">
<mat-card class="setting-card">
<mat-card-content>
<div class="setting-header">
<h3>Enable Plex</h3>
<mat-slide-toggle [id]="'enable'" [(ngModel)]="settings.enable"></mat-slide-toggle>
</div>
</mat-card-content>
</mat-card>
<mat-card class="setting-card">
<mat-card-content>
<div class="setting-header">
<h3>Enable User Watchlist Requests</h3>
<mat-slide-toggle [id]="'enableWatchlistImport'" [(ngModel)]="settings.enableWatchlistImport"></mat-slide-toggle>
</div>
<p class="setting-description">
When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled.
This <strong>only</strong> applies to users that are logging in with their Plex Account.
<br>Request limits if set are all still applied
</p>
</mat-card-content>
</mat-card>
<mat-card class="setting-card" [class.disabled]="!settings.enableWatchlistImport">
<mat-card-content>
<div class="setting-header">
<h3>Request Whole Show</h3>
<mat-slide-toggle [id]="'monitorAll'" [(ngModel)]="settings.monitorAll" [disabled]="!settings.enableWatchlistImport"></mat-slide-toggle>
</div>
<p class="setting-description">
If enabled then watchlist requests for TV Shows will request the <strong>whole</strong> show.
If not enabled it will only request the latest season.
</p>
</mat-card-content>
</mat-card>
<mat-card class="setting-card" [class.disabled]="!settings.enableWatchlistImport">
<mat-card-content>
<div class="setting-header">
<h3>Notify on Token Expiration</h3>
<mat-slide-toggle [id]="'notifyOnWatchlistTokenExpiration'" [(ngModel)]="settings.notifyOnWatchlistTokenExpiration" [disabled]="!settings.enableWatchlistImport"></mat-slide-toggle>
</div>
<p class="setting-description">
When enabled, users will receive a notification if their Plex watchlist token expires and they need to log into Ombi again to continue using the watchlist feature.
<br><strong>Note:</strong> This requires email notifications to be configured in the notification settings, and users must have an email address set on their account to receive these notifications.
</p>
</mat-card-content>
</mat-card>
</div>
<mat-card class="info-banner">
<mat-icon color="primary" style="margin-right: 12px;">info</mat-icon>
<span style="flex:1;">
Some users may need to re-log in to use the watchlist feature.
</span>
<button mat-button color="accent" (click)="openWatchlistUserLog()">
View Users
</button>
</mat-card>
</div> </div>
</div> <!-- Main Content Area -->
<settings-plex-form-field [label]="'Enable'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enable"></settings-plex-form-field> <div class="main-content">
<!-- Left Column - Servers and Actions -->
<div class="content-column">
<!-- Servers Section -->
<div class="settings-section">
<h2>Plex Servers</h2>
<div class="servers-grid">
<mat-card class="server-card" *ngFor="let server of settings.servers">
<mat-card-content>
<button mat-button (click)="edit(server)" [id]="server.name + '-button'">
<mat-icon>dns</mat-icon>
<span>{{server.name}}</span>
</button>
</mat-card-content>
</mat-card>
<settings-plex-form-field [label]="'Enable User Watchlist Requests'" [type]="'checkbox'" [id]="'enableWatchlistImport'" [(value)]="settings.enableWatchlistImport"> <mat-card class="server-card new-server">
<small bottom>When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled. This <b>only</b> applies to users that are logging in with their Plex Account <mat-card-content>
<br>Request limits if set are all still applied <button mat-button (click)="newServer()" id="newServer">
</small> <mat-icon>add_circle</mat-icon>
</settings-plex-form-field> <span>Add Server</span>
</button>
</mat-card-content>
</mat-card>
</div>
</div>
<settings-plex-form-field [disabled]="!settings.enableWatchlistImport" [label]="'Watchlist - Request Whole Show'" disabled [type]="'checkbox'" [id]="'monitorAll'" [(value)]="settings.monitorAll"> <!-- Sync Actions Section -->
<small bottom>If enabled then watchlist requests for TV Shows will request the <strong><em>whole</em></strong> show. If not enabled it will only request the latest season. <div class="settings-section">
</small> <h2>Sync Actions</h2>
</settings-plex-form-field> <div class="sync-actions-grid">
<button mat-stroked-button (click)="runSync(PlexSyncType.Full)" id="fullSync">
<mat-icon>sync</mat-icon>
Full Sync
</button>
<button mat-stroked-button (click)="runSync(PlexSyncType.RecentlyAdded)" id="recentlyAddedSync">
<mat-icon>update</mat-icon>
Partial Sync
</button>
<button mat-stroked-button (click)="runSync(PlexSyncType.ClearAndReSync)" id="clearData">
<mat-icon>cleaning_services</mat-icon>
Clear & Resync
</button>
<button mat-stroked-button (click)="runSync(PlexSyncType.WatchlistImport)" id="watchlistImport">
<mat-icon>playlist_add</mat-icon>
Run Watchlist Import
</button>
</div>
</div>
</div>
<settings-plex-form-field [disabled]="!settings.enableWatchlistImport" [label]="'Notify Users on Watchlist Token Expiration'" [type]="'checkbox'" [id]="'notifyOnWatchlistTokenExpiration'" [(value)]="settings.notifyOnWatchlistTokenExpiration"> <!-- Right Column - Plex Credentials -->
<small bottom>When enabled, users will receive a notification if their Plex watchlist token expires and they need to log into Ombi again to continue using the watchlist feature. <div class="content-column">
<br><strong>Note:</strong> This requires email notifications to be configured in the notification settings, and users must have an email address set on their account to receive these notifications. <div class="settings-section">
</small> <h2>Plex Credentials</h2>
</settings-plex-form-field> <mat-card class="credentials-card">
<mat-card-content>
<p class="credentials-description">
These fields are optional to automatically fill in your Plex server settings.
<br>This will pass your username and password to the Plex.tv API to grab the servers associated with this user.
<br>If you have 2FA enabled on your account, you need to append the 2FA code to the end of your password.
</p>
<hr> <mat-form-field appearance="outline" class="full-width">
<mat-label>Username</mat-label>
<input matInput [id]="'username'" [(ngModel)]="username">
</mat-form-field>
<div class="row"> <mat-form-field appearance="outline" class="full-width">
<div class="col-md-7"> <mat-label>Password</mat-label>
<h2 style="margin: 1em 0 0 0;">Servers</h2> <input matInput [id]="'password'" type="password" [(ngModel)]="password">
<mat-list style="display:flex; flex-flow: wrap;"> </mat-form-field>
<mat-card class="server-card" *ngFor="let server of settings.servers">
<button mat-button (click)="edit(server)" id="{{server.name}}-button">
<h3>{{server.name}}</h3>
</button>
</mat-card>
<mat-card class="server-card new-server-card"> <button mat-raised-button color="primary" id="loadServers" (click)="requestServers()" class="full-width">
<button mat-button (click)="newServer()" id="newServer"> <mat-icon>key</mat-icon>
<i class="fas fa-plus fa-xl"></i> Load Servers
<h3>Manually Add Server</h3> </button>
</button>
</mat-card>
</mat-list>
<mat-form-field appearance="outline" class="full-width mt-3">
<div class="row"> <mat-label>Select Server</mat-label>
<mat-select [id]="'servers'" *ngIf="loadedServers">
<br /> <mat-option (click)="selectServer(s)" *ngFor="let s of loadedServers.servers.server" [value]="s.server">
<div class="form-group col-2"> {{s.name}}
<button mat-raised-button (click)="runSync(PlexSyncType.Full)" type="button" id="fullSync" </mat-option>
class="mat-focus-indicator mat-stroked-button mat-button-base">Full </mat-select>
Sync</button><br /> <input matInput disabled placeholder="No Servers Loaded" *ngIf="!loadedServers">
</div> </mat-form-field>
<div class="form-group col-2"> </mat-card-content>
<button mat-raised-button (click)="runSync(PlexSyncType.RecentlyAdded)" type="button" id="recentlyAddedSync" </mat-card>
class="mat-focus-indicator mat-stroked-button mat-button-base">Partial Sync</button>
</div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.ClearAndReSync)" type="button" id="clearData"
class="mat-focus-indicator mat-stroked-button mat-button-base">
Clear Data And Resync
</button>
</div>
<div class="form-group col-12">
<button mat-raised-button (click)="runSync(PlexSyncType.WatchlistImport)" type="button" id="watchlistImport"
class="mat-focus-indicator mat-stroked-button mat-button-base">
Run Watchlist Import
</button>
</div>
</div>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<div>
<button mat-raised-button (click)="save()" type="submit" id="save"
class="mat-focus-indicator mat-raised-button mat-button-base mat-accent">Submit</button>
</div> </div>
</div> </div>
</div> </div>
</div> </mat-card-content>
</div>
<div class="col-md-5"> <mat-card-actions align="end">
<div class="md-form-field"> <button mat-raised-button color="accent" (click)="save()" id="save">
<label for="username" class="control-label"> <mat-icon>save</mat-icon>
<h3>Plex Credentials</h3> Save Changes
<small>These fields are optional to automatically fill in your Plex server settings. <br> </button>
This will pass your username and password to the Plex.tv API to grab the servers associated with this user. </mat-card-actions>
<br> </mat-card>
If you have 2FA enabled on your account, you need to append the 2FA code to the end of your password.</small>
</label>
</div>
<settings-plex-form-field [label]="'Username'" [id]="'username'" [(value)]="username"></settings-plex-form-field>
<settings-plex-form-field [label]="'Password'" [id]="'password'" [type]="'password'" [(value)]="password"></settings-plex-form-field>
<div class="md-form-field">
<div class="right">
<button mat-raised-button id="loadServers" (click)="requestServers()"
class="mat-stroked-button">Load Servers
<i class="fas fa-key"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-2 align-self-center">
Please select the server:
</div>
<div class="md-form-field col-10">
<div *ngIf="!loadedServers">
<mat-form-field appearance="outline" floatLabel=auto>
<input disabled matInput placeholder="No Servers Loaded" id="servers">
</mat-form-field>
</div>
<div *ngIf="loadedServers">
<mat-form-field appearance="outline">
<mat-select placeholder="Servers Loaded! Please Select" id="servers">
<mat-option (click)="selectServer(s)"
*ngFor="let s of loadedServers.servers.server" [value]="s.server">
{{s.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</div>
</div> </div>
</fieldset>
</div>
<!--(){{settings|json}}-->

View file

@ -43,4 +43,258 @@
h3 { h3 {
margin: 0; margin: 0;
} }
} }
.plex-settings-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.settings-card {
margin-bottom: 20px;
background: transparent;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.25);
border-radius: 12px;
border: 1px solid #353a45;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 24px;
margin-bottom: 30px;
}
.setting-card {
height: 100%;
background: transparent;
border-radius: 10px;
border: 1px solid #353a45;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
color: #f1f3f6;
}
.setting-card.disabled {
opacity: 0.6;
}
.setting-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-weight: 600;
color: #fff;
}
.setting-description {
color: #e0e3ea;
font-size: 15px;
margin: 0;
font-weight: 400;
line-height: 1.6;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 36px;
margin-top: 36px;
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px;
}
.server-card {
text-align: center;
background: transparent;
border-radius: 10px;
border: 1px solid #353a45;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
color: #f1f3f6;
}
.server-card button {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: #f1f3f6;
font-weight: 500;
}
.server-card mat-icon {
font-size: 32px;
height: 32px;
width: 32px;
margin-bottom: 10px;
color: #90caf9;
}
.sync-actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
}
.sync-actions-grid button {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
color: #f1f3f6;
background: transparent;
border: 1px solid #353a45;
border-radius: 10px;
font-weight: 500;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
}
.sync-actions-grid mat-icon {
font-size: 32px;
height: 32px;
width: 32px;
margin-bottom: 10px;
color: #90caf9;
}
.credentials-card {
padding: 24px;
background: transparent;
border-radius: 10px;
border: 1px solid #353a45;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.18);
color: #f1f3f6;
}
.credentials-description {
color: #e0e3ea;
margin-bottom: 20px;
font-size: 15px;
font-weight: 400;
}
mat-card-title, h2, h3 {
color: #fff !important;
font-weight: 700 !important;
letter-spacing: 0.5px;
}
mat-card-header {
border-bottom: 1px solid #353a45;
margin-bottom: 16px;
}
mat-form-field {
color: #f1f3f6 !important;
}
mat-label, .mat-form-field-label {
color: #b0b6c3 !important;
font-weight: 500;
}
input[matInput], .mat-input-element {
color: #f1f3f6 !important;
background: transparent !important;
}
mat-select {
color: #f1f3f6 !important;
background: transparent !important;
}
.full-width {
width: 100%;
}
.mt-3 {
margin-top: 1rem;
}
.mat-slide-toggle.mat-checked .mat-slide-toggle-bar {
background-color: #90caf9 !important;
}
.mat-slide-toggle-thumb {
background-color: #2196f3 !important;
}
button[mat-flat-button], button[mat-raised-button], button[mat-stroked-button], button[mat-button] {
font-weight: 600;
letter-spacing: 0.2px;
color: #f1f3f6;
background: #2196f3;
border-radius: 6px;
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.12);
transition: background 0.2s;
}
button[mat-flat-button]:hover, button[mat-raised-button]:hover, button[mat-stroked-button]:hover, button[mat-button]:hover {
background: #42a5f5;
}
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.settings-grid {
grid-template-columns: 1fr;
}
.servers-grid {
grid-template-columns: 1fr;
}
.sync-actions-grid {
grid-template-columns: 1fr;
}
}
.watchlist-errors-btn-row {
display: flex;
justify-content: center;
margin-top: 18px;
}
.info-banner {
display: flex;
align-items: center;
background: #23272f;
color: #e0e3ea;
border: 1px solid #1976d2;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.12);
border-radius: 8px;
padding: 16px 20px;
margin-top: 18px;
margin-bottom: 0;
font-size: 16px;
font-weight: 500;
gap: 12px;
}
.info-banner mat-icon {
font-size: 28px;
color: #42a5f5;
}
.info-banner button[mat-button] {
margin-left: 16px;
font-weight: 600;
}