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

View file

@ -11,3 +11,95 @@
.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>
<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> </div>
<settings-plex-form-field [label]="'Enable'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enable"></settings-plex-form-field> </mat-card-content>
</mat-card>
<settings-plex-form-field [label]="'Enable User Watchlist Requests'" [type]="'checkbox'" [id]="'enableWatchlistImport'" [(value)]="settings.enableWatchlistImport"> <mat-card class="setting-card">
<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>
<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 <br>Request limits if set are all still applied
</small> </p>
</settings-plex-form-field> </mat-card-content>
</mat-card>
<settings-plex-form-field [disabled]="!settings.enableWatchlistImport" [label]="'Watchlist - Request Whole Show'" disabled [type]="'checkbox'" [id]="'monitorAll'" [(value)]="settings.monitorAll"> <mat-card class="setting-card" [class.disabled]="!settings.enableWatchlistImport">
<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. <mat-card-content>
</small> <div class="setting-header">
</settings-plex-form-field> <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>
<settings-plex-form-field [disabled]="!settings.enableWatchlistImport" [label]="'Notify Users on Watchlist Token Expiration'" [type]="'checkbox'" [id]="'notifyOnWatchlistTokenExpiration'" [(value)]="settings.notifyOnWatchlistTokenExpiration"> <mat-card class="setting-card" [class.disabled]="!settings.enableWatchlistImport">
<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. <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. <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.
</small> </p>
</settings-plex-form-field> </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>
<hr> <!-- Main Content Area -->
<div class="main-content">
<div class="row"> <!-- Left Column - Servers and Actions -->
<div class="col-md-7"> <div class="content-column">
<h2 style="margin: 1em 0 0 0;">Servers</h2> <!-- Servers Section -->
<mat-list style="display:flex; flex-flow: wrap;"> <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 class="server-card" *ngFor="let server of settings.servers">
<button mat-button (click)="edit(server)" id="{{server.name}}-button"> <mat-card-content>
<h3>{{server.name}}</h3> <button mat-button (click)="edit(server)" [id]="server.name + '-button'">
<mat-icon>dns</mat-icon>
<span>{{server.name}}</span>
</button> </button>
</mat-card-content>
</mat-card> </mat-card>
<mat-card class="server-card new-server-card"> <mat-card class="server-card new-server">
<mat-card-content>
<button mat-button (click)="newServer()" id="newServer"> <button mat-button (click)="newServer()" id="newServer">
<i class="fas fa-plus fa-xl"></i> <mat-icon>add_circle</mat-icon>
<h3>Manually Add Server</h3> <span>Add Server</span>
</button> </button>
</mat-card-content>
</mat-card> </mat-card>
</mat-list>
<div class="row">
<br />
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.Full)" type="button" id="fullSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Full
Sync</button><br />
</div> </div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.RecentlyAdded)" type="button" id="recentlyAddedSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Partial Sync</button>
</div> </div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.ClearAndReSync)" type="button" id="clearData" <!-- Sync Actions Section -->
class="mat-focus-indicator mat-stroked-button mat-button-base"> <div class="settings-section">
Clear Data And Resync <h2>Sync Actions</h2>
<div class="sync-actions-grid">
<button mat-stroked-button (click)="runSync(PlexSyncType.Full)" id="fullSync">
<mat-icon>sync</mat-icon>
Full Sync
</button> </button>
</div> <button mat-stroked-button (click)="runSync(PlexSyncType.RecentlyAdded)" id="recentlyAddedSync">
<div class="form-group col-12"> <mat-icon>update</mat-icon>
<button mat-raised-button (click)="runSync(PlexSyncType.WatchlistImport)" type="button" id="watchlistImport" Partial Sync
class="mat-focus-indicator mat-stroked-button mat-button-base"> </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 Run Watchlist Import
</button> </button>
</div> </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> <!-- Right Column - Plex Credentials -->
<div class="content-column">
<div class="settings-section">
<h2>Plex Credentials</h2>
<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>
<div class="col-md-5"> <mat-form-field appearance="outline" class="full-width">
<div class="md-form-field"> <mat-label>Username</mat-label>
<label for="username" class="control-label"> <input matInput [id]="'username'" [(ngModel)]="username">
<h3>Plex Credentials</h3> </mat-form-field>
<small>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.</small>
</label>
</div>
<settings-plex-form-field [label]="'Username'" [id]="'username'" [(value)]="username"></settings-plex-form-field> <mat-form-field appearance="outline" class="full-width">
<settings-plex-form-field [label]="'Password'" [id]="'password'" [type]="'password'" [(value)]="password"></settings-plex-form-field> <mat-label>Password</mat-label>
<input matInput [id]="'password'" type="password" [(ngModel)]="password">
</mat-form-field>
<div class="md-form-field"> <button mat-raised-button color="primary" id="loadServers" (click)="requestServers()" class="full-width">
<div class="right"> <mat-icon>key</mat-icon>
<button mat-raised-button id="loadServers" (click)="requestServers()" Load Servers
class="mat-stroked-button">Load Servers
<i class="fas fa-key"></i>
</button> </button>
</div>
</div>
<div class="row"> <mat-form-field appearance="outline" class="full-width mt-3">
<div class="col-2 align-self-center"> <mat-label>Select Server</mat-label>
Please select the server: <mat-select [id]="'servers'" *ngIf="loadedServers">
</div> <mat-option (click)="selectServer(s)" *ngFor="let s of loadedServers.servers.server" [value]="s.server">
<div class="md-form-field col-10"> {{s.name}}
<div *ngIf="!loadedServers"> </mat-option>
<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-select>
<input matInput disabled placeholder="No Servers Loaded" *ngIf="!loadedServers">
</mat-form-field> </mat-form-field>
</mat-card-content>
</mat-card>
</div> </div>
</div> </div>
</div> </div>
</div> </mat-card-content>
</div>
</fieldset> <mat-card-actions align="end">
<button mat-raised-button color="accent" (click)="save()" id="save">
<mat-icon>save</mat-icon>
Save Changes
</button>
</mat-card-actions>
</mat-card>
</div> </div>
<!--(){{settings|json}}-->

View file

@ -44,3 +44,257 @@
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;
}