diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 92597f903..14dab489e 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -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)); } diff --git a/API/Controllers/PanelsController.cs b/API/Controllers/PanelsController.cs new file mode 100644 index 000000000..809b778f2 --- /dev/null +++ b/API/Controllers/PanelsController.cs @@ -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; + +/// +/// For the Panels app explicitly +/// +[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 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(); + } +} diff --git a/API/Helpers/SmartFilterHelper.cs b/API/Helpers/SmartFilterHelper.cs index 30b66c3ee..740b8cd4e 100644 --- a/API/Helpers/SmartFilterHelper.cs +++ b/API/Helpers/SmartFilterHelper.cs @@ -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 statements) diff --git a/UI/Web/src/app/app.component.html b/UI/Web/src/app/app.component.html index 96c8833f6..12a48e5f8 100644 --- a/UI/Web/src/app/app.component.html +++ b/UI/Web/src/app/app.component.html @@ -4,7 +4,7 @@ -
+
diff --git a/UI/Web/src/app/app.component.scss b/UI/Web/src/app/app.component.scss index f36dbb0c3..396539e99 100644 --- a/UI/Web/src/app/app.component.scss +++ b/UI/Web/src/app/app.component.scss @@ -38,3 +38,17 @@ width: auto; } } + + +@keyframes fadeInUp { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.fadein { + animation: 2s fadeInUp; +} diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index a23635277..36fac866d 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -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; + fade = 'out'; private readonly destroyRef = inject(DestroyRef); private readonly offcanvas = inject(NgbOffcanvas); diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss index f9d45420e..ffc87dadc 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss @@ -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; diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html index 8077add2f..5691d1ac1 100644 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html +++ b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index fc314f8db..3361064bb 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -24,7 +24,7 @@ -
+
*', [ + 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; + 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) { - // 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) { - // 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) { + // // 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) { + // // 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() { diff --git a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html index 8a7847511..ceceab2b3 100644 --- a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html +++ b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.html @@ -15,6 +15,17 @@