Compare commits
8 commits
develop
...
feature/cs
Author | SHA1 | Date | |
---|---|---|---|
![]() |
675136a18c | ||
![]() |
e705aadaac | ||
![]() |
b4a07d8058 | ||
![]() |
d328528e6e | ||
![]() |
0e43dae3d1 | ||
![]() |
34282f3222 | ||
![]() |
f7c1e37694 | ||
![]() |
c62e48f06a |
14 changed files with 201 additions and 48 deletions
|
@ -1102,14 +1102,21 @@ public class OpdsController : BaseApiController
|
|||
Response.AddCacheHeader(content);
|
||||
|
||||
// Save progress for the user
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
|
||||
var userAgent = Request.Headers["User-Agent"].ToString();
|
||||
Console.WriteLine("User Agent: " + userAgent);
|
||||
if (!userAgent.Contains("panels", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ChapterId = chapterId,
|
||||
PageNum = pageNumber,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = volumeId,
|
||||
LibraryId =libraryId
|
||||
}, await GetUser(apiKey));
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
ChapterId = chapterId,
|
||||
PageNum = pageNumber,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = volumeId,
|
||||
LibraryId =libraryId
|
||||
}, await GetUser(apiKey));
|
||||
}
|
||||
|
||||
|
||||
return File(content, MimeTypeMap.GetMimeType(format));
|
||||
}
|
||||
|
|
33
API/Controllers/PanelsController.cs
Normal file
33
API/Controllers/PanelsController.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// For the Panels app explicitly
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class PanelsController : BaseApiController
|
||||
{
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public PanelsController(IReaderService readerService, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_readerService = readerService;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
[HttpPost("save-progress")]
|
||||
public async Task<ActionResult> SaveProgress(ProgressDto dto, [FromQuery] string apiKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required");
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
await _readerService.SaveReadingProgress(dto, userId);
|
||||
return Ok();
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ public static class SmartFilterHelper
|
|||
|
||||
private static string EncodeSortOptions(SortOptions sortOptions)
|
||||
{
|
||||
return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField}&isAscending={sortOptions.IsAscending}");
|
||||
return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField},isAscending={sortOptions.IsAscending}");
|
||||
}
|
||||
|
||||
private static string EncodeFilterStatementDtos(ICollection<FilterStatementDto> statements)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<a id="content"></a>
|
||||
<app-side-nav *ngIf="navService.sideNavVisibility$ | async"></app-side-nav>
|
||||
|
||||
<div class="container-fluid" [ngClass]="{'g-0': (navService.sideNavVisibility$ | async) === false}">
|
||||
<div class="container-fluid fadein" [ngClass]="{'g-0': (navService.sideNavVisibility$ | async) === false}">
|
||||
<div style="padding: 20px 0 0;" *ngIf="navService.sideNavVisibility$ | async else noSideNav">
|
||||
<div class="companion-bar" [ngClass]="{'companion-bar-content': (navService.sideNavCollapsed$ | async) === false}">
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
@ -38,3 +38,17 @@
|
|||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fadein {
|
||||
animation: 2s fadeInUp;
|
||||
}
|
||||
|
|
|
@ -12,17 +12,30 @@ import {ThemeService} from "./_services/theme.service";
|
|||
import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.component';
|
||||
import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {animate, state, style, transition, trigger} from "@angular/animations";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
animations: [
|
||||
trigger('fadeIn', [
|
||||
// the "out" style determines the "resting" state of the element when it is visible.
|
||||
state('out', style({opacity: 0})),
|
||||
// in state
|
||||
state('in', style({opacity: 1})),
|
||||
|
||||
transition('out => in', animate('700ms ease-in')),
|
||||
transition('in => out', animate('700ms ease-in')),
|
||||
])
|
||||
],
|
||||
imports: [NgClass, NgIf, SideNavComponent, RouterOutlet, AsyncPipe, NavHeaderComponent]
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
transitionState$!: Observable<boolean>;
|
||||
fade = 'out';
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly offcanvas = inject(NgbOffcanvas);
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
text-shadow: 1.3px 0.5px 1px rgba(255, 255, 255, 0.4); // TODO: perfect this
|
||||
color: hsla(0,0%,100%,.7);
|
||||
height: 25px;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *transloco="let t; read: 'series-info-cards'">
|
||||
<div class="row g-0 mt-3">
|
||||
<div class="row justify-content-between g-0 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-template #filterSection>
|
||||
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded && filterV2">
|
||||
<div class="filter-section mx-auto pb-3 slide-down" *ngIf="fullyLoaded && filterV2" [@slideFromTop]="isOpen">
|
||||
<div class="row justify-content-center g-0">
|
||||
<app-metadata-builder [filter]="filterV2"
|
||||
[availableFilterFields]="allFilterFields"
|
||||
|
|
|
@ -38,6 +38,9 @@ import {
|
|||
Select2UpdateValue
|
||||
} from "ng-select2-component";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
import {animate, state, style, transition, trigger} from "@angular/animations";
|
||||
|
||||
const ANIMATION_SPEED = 200;
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-filter',
|
||||
|
@ -46,7 +49,29 @@ import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
|
||||
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module, NgClass]
|
||||
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module, NgClass],
|
||||
animations: [
|
||||
trigger('slideFromTop', [
|
||||
state('in', style({ transform: 'translateY(0)' })),
|
||||
transition('void => *', [
|
||||
style({ transform: 'translateY(-100%)' }),
|
||||
animate(ANIMATION_SPEED)
|
||||
]),
|
||||
transition('* => void', [
|
||||
animate(ANIMATION_SPEED, style({ transform: 'translateY(-100%)' })),
|
||||
])
|
||||
]),
|
||||
trigger('slideFromBottom', [
|
||||
state('in', style({ transform: 'translateY(0)' })),
|
||||
transition('void => *', [
|
||||
style({ transform: 'translateY(100%)' }),
|
||||
animate(ANIMATION_SPEED)
|
||||
]),
|
||||
transition('* => void', [
|
||||
animate(ANIMATION_SPEED, style({ transform: 'translateY(100%)' })),
|
||||
])
|
||||
])
|
||||
],
|
||||
})
|
||||
export class MetadataFilterComponent implements OnInit {
|
||||
|
||||
|
@ -89,6 +114,8 @@ export class MetadataFilterComponent implements OnInit {
|
|||
|
||||
smartFilters!: Array<Select2Option>;
|
||||
|
||||
isOpen = false;
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
||||
|
@ -114,44 +141,47 @@ export class MetadataFilterComponent implements OnInit {
|
|||
this.filterOpen.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(openState => {
|
||||
this.filteringCollapsed = !openState;
|
||||
this.toggleService.set(!this.filteringCollapsed);
|
||||
this.isOpen = openState;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.loadFromPresetsAndSetup();
|
||||
}
|
||||
|
||||
loadSavedFilter(event: Select2UpdateEvent<any>) {
|
||||
// Load the filter from the backend and update the screen
|
||||
if (event.value === undefined || typeof(event.value) === 'string') return;
|
||||
const smartFilter = event.value as SmartFilter;
|
||||
this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
|
||||
this.cdRef.markForCheck();
|
||||
console.log('update event: ', event);
|
||||
}
|
||||
|
||||
createFilterValue(event: Select2AutoCreateEvent<any>) {
|
||||
// Create a new name and filter
|
||||
if (!this.filterV2) return;
|
||||
this.filterV2.name = event.value;
|
||||
this.filterService.saveFilter(this.filterV2).subscribe(() => {
|
||||
|
||||
const item = {
|
||||
value: {
|
||||
filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
|
||||
name: event.value,
|
||||
} as SmartFilter,
|
||||
label: event.value
|
||||
};
|
||||
this.smartFilters.push(item);
|
||||
this.sortGroup.get('name')?.setValue(item);
|
||||
this.cdRef.markForCheck();
|
||||
this.toastr.success(translate('toasts.smart-filter-updated'));
|
||||
this.apply();
|
||||
});
|
||||
|
||||
console.log('create event: ', event);
|
||||
}
|
||||
// loadSavedFilter(event: Select2UpdateEvent<any>) {
|
||||
// // Load the filter from the backend and update the screen
|
||||
// if (event.value === undefined || typeof(event.value) === 'string') return;
|
||||
// const smartFilter = event.value as SmartFilter;
|
||||
// this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
|
||||
// this.cdRef.markForCheck();
|
||||
// console.log('update event: ', event);
|
||||
// }
|
||||
//
|
||||
// createFilterValue(event: Select2AutoCreateEvent<any>) {
|
||||
// // Create a new name and filter
|
||||
// if (!this.filterV2) return;
|
||||
// this.filterV2.name = event.value;
|
||||
// this.filterService.saveFilter(this.filterV2).subscribe(() => {
|
||||
//
|
||||
// const item = {
|
||||
// value: {
|
||||
// filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
|
||||
// name: event.value,
|
||||
// } as SmartFilter,
|
||||
// label: event.value
|
||||
// };
|
||||
// this.smartFilters.push(item);
|
||||
// this.sortGroup.get('name')?.setValue(item);
|
||||
// this.cdRef.markForCheck();
|
||||
// this.toastr.success(translate('toasts.smart-filter-updated'));
|
||||
// this.apply();
|
||||
// });
|
||||
//
|
||||
// console.log('create event: ', event);
|
||||
// }
|
||||
|
||||
|
||||
close() {
|
||||
|
|
|
@ -15,6 +15,17 @@
|
|||
</div>
|
||||
<div class="dropdown" *ngIf="hasFocus">
|
||||
<ul class="list-group" role="listbox" id="dropdown">
|
||||
|
||||
<ng-container *ngIf="groupedData.series.length === 0 && !hasData">
|
||||
<li class="list-group-item section-header"><h6>Stumped on what to Type?</h6></li>
|
||||
<ul class="list-group results" role="group">
|
||||
<li class="list-group-item" style="font-size: 14px">
|
||||
<i>Try batman, 2014, Ecchi</i> <!-- Or maybe it should be Search by x, y, z -->
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="seriesTemplate !== undefined && groupedData.series.length > 0">
|
||||
<li class="list-group-item section-header"><h5 id="series-group">Series</h5></li>
|
||||
<ul class="list-group results" role="group" aria-describedby="series-group">
|
||||
|
|
|
@ -209,9 +209,9 @@ export class FilterUtilitiesService {
|
|||
}
|
||||
|
||||
decodeFilterStatements(encodedStatements: string): FilterStatement[] {
|
||||
const statementStrings = decodeURIComponent(encodedStatements).split(',');
|
||||
const statementStrings = decodeURIComponent(encodedStatements).split(',').map(s => decodeURIComponent(s));
|
||||
return statementStrings.map(statementString => {
|
||||
const parts = statementString.split('&');
|
||||
const parts = statementString.split(',');
|
||||
if (parts === null || parts.length < 3) return null;
|
||||
|
||||
const comparisonStartToken = parts.find(part => part.startsWith('comparison='));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *transloco="let t; read: 'side-nav-companion-bar'">
|
||||
<div class="mt-0 d-flex justify-content-between align-items-center">
|
||||
<div class="mt-0 d-flex justify-content-between align-items-center shadow-sm">
|
||||
<div>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<ng-content select="[subtitle]"></ng-content>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<i class="fa-solid fa-sliders" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('page-settings-title')}}</span>
|
||||
</button>
|
||||
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'secondary'}} btn-small" (click)="toggleService.toggle()"
|
||||
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'info'}} btn-small" (click)="toggleService.toggle()"
|
||||
[attr.aria-expanded]="filterOpen" placement="left"
|
||||
id="filter-btn--komf"
|
||||
ngbTooltip="{{filterOpen ? t('open-filter-and-sort') : t('close-filter-and-sort')}}"
|
||||
|
|
46
openapi.json
46
openapi.json
|
@ -7,7 +7,7 @@
|
|||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.9.4"
|
||||
"version": "0.7.10.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -4168,6 +4168,46 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/Panels/save-progress": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Panels"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProgressDto"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProgressDto"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProgressDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Plugin/authenticate": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
@ -19687,6 +19727,10 @@
|
|||
"name": "Image",
|
||||
"description": "Responsible for servicing up images stored in Kavita for entities"
|
||||
},
|
||||
{
|
||||
"name": "Panels",
|
||||
"description": "For the Panels app explicitly"
|
||||
},
|
||||
{
|
||||
"name": "Rating",
|
||||
"description": "Responsible for providing external ratings for Series"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue