diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml deleted file mode 100644 index 5bd9f3fa5..000000000 --- a/.github/workflows/draft-release.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Draft Release - -on: - push: - branches: - - master - -jobs: - draft-release: - name: Update Draft Release - runs-on: ubuntu-latest - - permissions: - contents: write - pull-requests: read - - steps: - - name: Draft Release Notes - uses: release-drafter/release-drafter@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/KoreaderUser.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/KoreaderUser.java index e1835e3e4..8203f74a8 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/KoreaderUser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/KoreaderUser.java @@ -12,4 +12,5 @@ public class KoreaderUser { private String password; private String passwordMD5; private boolean syncEnabled; + private boolean syncWithBookloreReader; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java index 596a6ca02..b95a680b9 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/ReadingSessionRepository.java @@ -25,24 +25,23 @@ public interface ReadingSessionRepository extends JpaRepository findSessionCountsByUserAndYear(@Param("userId") Long userId, @Param("year") int year); - @Query(""" - SELECT - b.id as bookId, - COALESCE(b.metadata.title, - (SELECT bf.fileName FROM BookFileEntity bf WHERE bf.book.id = b.id ORDER BY bf.id ASC LIMIT 1), - 'Unknown Book') as bookTitle, - rs.bookType as bookFileType, - MIN(rs.startTime) as startDate, - MAX(rs.endTime) as endDate, - COUNT(rs) as totalSessions, - SUM(rs.durationSeconds) as totalDurationSeconds - FROM ReadingSessionEntity rs - JOIN rs.book b - WHERE rs.user.id = :userId - AND rs.startTime >= :startOfWeek AND rs.startTime < :endOfWeek - GROUP BY b.id, b.metadata.title, rs.bookType, CAST(rs.startTime AS LocalDate) - ORDER BY MIN(rs.startTime) - """) + @Query(""" + SELECT + b.id as bookId, + COALESCE(b.metadata.title, + (SELECT bf.fileName FROM BookFileEntity bf WHERE bf.book.id = b.id ORDER BY bf.id ASC LIMIT 1), + 'Unknown Book') as bookTitle, + rs.bookType as bookFileType, + rs.startTime as startDate, + rs.endTime as endDate, + 1L as totalSessions, + rs.durationSeconds as totalDurationSeconds + FROM ReadingSessionEntity rs + JOIN rs.book b + WHERE rs.user.id = :userId + AND rs.startTime >= :startOfWeek AND rs.startTime < :endOfWeek + ORDER BY rs.startTime + """) List findSessionTimelineByUserAndWeek( @Param("userId") Long userId, @Param("startOfWeek") Instant startOfWeek, diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/controller/KoreaderUserControllerTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/controller/KoreaderUserControllerTest.java index 40c231293..641b4399b 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/controller/KoreaderUserControllerTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/controller/KoreaderUserControllerTest.java @@ -4,14 +4,18 @@ import com.adityachandel.booklore.model.dto.KoreaderUser; import com.adityachandel.booklore.service.koreader.KoreaderUserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; class KoreaderUserControllerTest { @@ -26,7 +30,7 @@ class KoreaderUserControllerTest { @BeforeEach void setUp() { try (AutoCloseable mocks = MockitoAnnotations.openMocks(this)) { - user = new KoreaderUser(1L, "testuser", "pass", "md5", true); + user = new KoreaderUser(1L, "testuser", "pass", "md5", true, true); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/KoreaderUserServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/KoreaderUserServiceTest.java index 3560908fd..831c81584 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/KoreaderUserServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/KoreaderUserServiceTest.java @@ -53,7 +53,7 @@ class KoreaderUserServiceTest { entity.setBookLoreUser(ownerEntity); entity.setUsername("kvUser"); - dto = new KoreaderUser(10L, "kvUser", null, null, false); + dto = new KoreaderUser(10L, "kvUser", null, null, false, true); when(koreaderUserMapper.toDto(any(KoreaderUserEntity.class))).thenReturn(dto); } @@ -69,7 +69,7 @@ class KoreaderUserServiceTest { when(koreaderUserMapper.toDto(any(KoreaderUserEntity.class))).thenAnswer(invocation -> { KoreaderUserEntity u = invocation.getArgument(0); - return new KoreaderUser(u.getId(), u.getUsername(), u.getPassword(), u.getPasswordMD5(), u.isSyncEnabled()); + return new KoreaderUser(u.getId(), u.getUsername(), u.getPassword(), u.getPasswordMD5(), u.isSyncEnabled(), u.isSyncWithBookloreReader()); }); KoreaderUser result = service.upsertUser("userA", "passA"); diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.html index cc1fac852..223baecd9 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.html @@ -36,7 +36,7 @@ {{ formatDate(session.startTime) }} {{ formatDate(session.endTime) }} - {{ formatDuration(session.durationSeconds) }} + {{ getActualDuration(session) }} @if (session.startProgress !== null && session.startProgress !== undefined && session.endProgress !== null && session.endProgress !== undefined) { {{ session.startProgress }}% → {{ session.endProgress }}% diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.ts index 44dee40c9..910812827 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-reading-sessions/book-reading-sessions.component.ts @@ -69,6 +69,24 @@ export class BookReadingSessionsComponent implements OnInit, OnChanges { return `${secs}s`; } + calculateActualDuration(session: ReadingSessionResponse): number { + const startTime = new Date(session.startTime).getTime(); + const endTime = new Date(session.endTime).getTime(); + return Math.floor((endTime - startTime) / 1000); + } + + getActualDuration(session: ReadingSessionResponse): string { + const actualDuration = this.calculateActualDuration(session); + const storedDuration = session.durationSeconds; + + if (Math.abs(actualDuration - storedDuration) > 1) { + // Discrepancy detected - show both values + return `${this.formatDuration(actualDuration)}`; + } + + return this.formatDuration(actualDuration); + } + formatDate(dateString: string): string { return new Date(dateString).toLocaleString(); } diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/footer/cbx-footer.component.scss b/booklore-ui/src/app/features/readers/cbx-reader/layout/footer/cbx-footer.component.scss index 146ee06e2..33f6a51bb 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/footer/cbx-footer.component.scss +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/footer/cbx-footer.component.scss @@ -1,3 +1,5 @@ +@use 'sass:color'; + // Variables $footer-bg: #1a1a1a; $text-primary: rgba(255, 255, 255, 0.95); @@ -95,7 +97,7 @@ $transition-fast: 150ms ease; } &:hover:not(:disabled) { - background: darken($active-color, 10%); + background: color.adjust($active-color, $lightness: -10%); } &:disabled { @@ -289,7 +291,7 @@ $transition-fast: 150ms ease; transition: all 0.2s ease; &:hover:not(:disabled) { - background: darken($active-color, 10%); + background: color.adjust($active-color, $lightness: -10%); } &:disabled { diff --git a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss index 6e959c552..480b8bc2a 100644 --- a/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss +++ b/booklore-ui/src/app/features/readers/cbx-reader/layout/quick-settings/cbx-quick-settings.component.scss @@ -1,3 +1,5 @@ +@use 'sass:color'; + // Variables $panel-bg: #1a1a1a; $text-primary: rgba(255, 255, 255, 0.95); @@ -134,7 +136,7 @@ $transition-normal: 200ms ease; background: rgba(255, 255, 255, 0.2); &.active { - background: lighten($active-color, 5%); + background: color.adjust($active-color, $lightness: 5%); } } } diff --git a/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.html b/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.html index 924e38d15..d868adc69 100644 --- a/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.html +++ b/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.html @@ -77,23 +77,21 @@ - @if (hardcoverEnabled) { -
-
-
- - -
- +
+
+
+ +
+
- } +
diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/author-universe-chart/author-universe-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/author-universe-chart/author-universe-chart.component.scss index b58a01494..de0f64dc0 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/author-universe-chart/author-universe-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/author-universe-chart/author-universe-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .author-universe-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/book-formats-chart/book-formats-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/book-formats-chart/book-formats-chart.component.scss index f28e80fc2..9b2a908a7 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/book-formats-chart/book-formats-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/book-formats-chart/book-formats-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .book-formats-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/language-chart/language-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/language-chart/language-chart.component.scss index 8e53810b8..bc08e75a1 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/language-chart/language-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/language-chart/language-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .language-chart-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/metadata-score-chart/metadata-score-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/metadata-score-chart/metadata-score-chart.component.scss index 437bb511e..62b41ccb4 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/metadata-score-chart/metadata-score-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/metadata-score-chart/metadata-score-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .metadata-score-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/page-count-chart/page-count-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/page-count-chart/page-count-chart.component.scss index bc1d30ae6..a7b82f593 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/page-count-chart/page-count-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/page-count-chart/page-count-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .page-count-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-timeline-chart/publication-timeline-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-timeline-chart/publication-timeline-chart.component.scss index b4729524d..fc46befcc 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-timeline-chart/publication-timeline-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-timeline-chart/publication-timeline-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .publication-timeline-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-trend-chart/publication-trend-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-trend-chart/publication-trend-chart.component.scss index 53b1578c5..707e29d98 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-trend-chart/publication-trend-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/publication-trend-chart/publication-trend-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .publication-trend-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/reading-journey-chart/reading-journey-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/reading-journey-chart/reading-journey-chart.component.scss index 77c7f23a2..72221f785 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/reading-journey-chart/reading-journey-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/reading-journey-chart/reading-journey-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .reading-journey-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/library-stats/charts/top-items-chart/top-items-chart.component.scss b/booklore-ui/src/app/features/stats/component/library-stats/charts/top-items-chart/top-items-chart.component.scss index a53ddd594..a035f6caf 100644 --- a/booklore-ui/src/app/features/stats/component/library-stats/charts/top-items-chart/top-items-chart.component.scss +++ b/booklore-ui/src/app/features/stats/component/library-stats/charts/top-items-chart/top-items-chart.component.scss @@ -1,4 +1,4 @@ -@import '../../../_chart-shared'; +@use '../../../_chart-shared'; .top-items-container { width: 100%; diff --git a/booklore-ui/src/app/features/stats/component/user-stats/charts/reading-session-timeline/reading-session-timeline.component.ts b/booklore-ui/src/app/features/stats/component/user-stats/charts/reading-session-timeline/reading-session-timeline.component.ts index 7464e59b5..2ddc9d4af 100644 --- a/booklore-ui/src/app/features/stats/component/user-stats/charts/reading-session-timeline/reading-session-timeline.component.ts +++ b/booklore-ui/src/app/features/stats/component/user-stats/charts/reading-session-timeline/reading-session-timeline.component.ts @@ -145,8 +145,8 @@ export class ReadingSessionTimelineComponent implements OnInit { response.forEach((item) => { const startTime = new Date(item.startDate); - const duration = item.totalDurationSeconds / 60; - const endTime = new Date(startTime.getTime() + item.totalDurationSeconds * 1000); + const endTime = item.endDate ? new Date(item.endDate) : new Date(startTime.getTime() + item.totalDurationSeconds * 1000); + const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60); sessions.push({ startTime, diff --git a/booklore-ui/src/app/shared/service/file-download.service.spec.ts b/booklore-ui/src/app/shared/service/file-download.service.spec.ts index baeaea561..a825596f2 100644 --- a/booklore-ui/src/app/shared/service/file-download.service.spec.ts +++ b/booklore-ui/src/app/shared/service/file-download.service.spec.ts @@ -1,4 +1,4 @@ -import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {beforeEach, describe, expect, it, vi, afterEach} from 'vitest'; import {TestBed} from '@angular/core/testing'; import {EnvironmentInjector, runInInjectionContext} from '@angular/core'; import {HttpClient, HttpEventType, HttpHeaders, HttpResponse} from '@angular/common/http'; @@ -38,6 +38,12 @@ describe('FileDownloadService', () => { const injector = TestBed.inject(EnvironmentInjector); service = runInInjectionContext(injector, () => TestBed.inject(FileDownloadService)); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.runAllTimers(); + vi.useRealTimers(); }); it('should trigger download and handle progress and completion', () => { @@ -58,15 +64,23 @@ describe('FileDownloadService', () => { // Mock DOM for download const appendChild = vi.fn(); const removeChild = vi.fn(); + const link = { + setAttribute: vi.fn(), + click: vi.fn(), + style: {}, + href: '', + download: '', + parentNode: { + removeChild: removeChild + } + }; (globalThis as any).document = { - createElement: vi.fn().mockReturnValue({ - setAttribute: vi.fn(), - click: vi.fn(), - style: {}, - href: '', - download: '' - }), - body: {appendChild, removeChild} + createElement: vi.fn().mockReturnValue(link), + body: { + appendChild: appendChild, + removeChild: removeChild, + contains: vi.fn().mockReturnValue(true) + } }; (globalThis as any).window = { URL: { @@ -134,6 +148,12 @@ describe('FileDownloadService - API Contract Tests', () => { const injector = TestBed.inject(EnvironmentInjector); service = runInInjectionContext(injector, () => TestBed.inject(FileDownloadService)); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.runAllTimers(); + vi.useRealTimers(); }); describe('API contract', () => { diff --git a/booklore-ui/src/app/shared/service/file-download.service.ts b/booklore-ui/src/app/shared/service/file-download.service.ts index 75206846a..5d86b706d 100644 --- a/booklore-ui/src/app/shared/service/file-download.service.ts +++ b/booklore-ui/src/app/shared/service/file-download.service.ts @@ -94,7 +94,13 @@ export class FileDownloadService { link.click(); setTimeout(() => { - document.body.removeChild(link); + try { + if (link && link.parentNode) { + link.parentNode.removeChild(link); + } + } catch (e) { + // Ignore errors during cleanup, may occur if DOM is not available + } window.URL.revokeObjectURL(objectUrl); }, 100); }