diff --git a/comixed-model/src/main/java/org/comixedproject/model/comicfiles/ComicFileGroup.java b/comixed-model/src/main/java/org/comixedproject/model/comicfiles/ComicFileGroup.java new file mode 100644 index 000000000..18bd93389 --- /dev/null +++ b/comixed-model/src/main/java/org/comixedproject/model/comicfiles/ComicFileGroup.java @@ -0,0 +1,48 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2021, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.model.comicfiles; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.comixedproject.views.View; + +/** + * ComicFileGroup holds a list of {@link ComicFile} instances that exist in the same + * directory. + * + * @author Darryl L. Pierce + */ +@RequiredArgsConstructor +public class ComicFileGroup { + @JsonProperty("directory") + @JsonView(View.ComicFileList.class) + @NonNull + @Getter + private String directory; + + @JsonProperty("files") + @JsonView(View.ComicFileList.class) + @Getter + private List files = new ArrayList<>(); +} diff --git a/comixed-model/src/main/java/org/comixedproject/model/net/comicfiles/LoadComicFilesResponse.java b/comixed-model/src/main/java/org/comixedproject/model/net/comicfiles/LoadComicFilesResponse.java index 734494f23..8be518b95 100644 --- a/comixed-model/src/main/java/org/comixedproject/model/net/comicfiles/LoadComicFilesResponse.java +++ b/comixed-model/src/main/java/org/comixedproject/model/net/comicfiles/LoadComicFilesResponse.java @@ -25,14 +25,14 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; -import org.comixedproject.model.comicfiles.ComicFile; +import org.comixedproject.model.comicfiles.ComicFileGroup; import org.comixedproject.views.View; @AllArgsConstructor public class LoadComicFilesResponse { - @JsonProperty("files") + @JsonProperty("groups") @Getter @Setter @JsonView(View.ComicFileList.class) - private List files = new ArrayList<>(); + private List groups = new ArrayList<>(); } diff --git a/comixed-rest/src/test/java/org/comixedproject/rest/comicfiles/ComicFileControllerTest.java b/comixed-rest/src/test/java/org/comixedproject/rest/comicfiles/ComicFileControllerTest.java index a1b99d4d7..1a429b3d8 100644 --- a/comixed-rest/src/test/java/org/comixedproject/rest/comicfiles/ComicFileControllerTest.java +++ b/comixed-rest/src/test/java/org/comixedproject/rest/comicfiles/ComicFileControllerTest.java @@ -24,7 +24,7 @@ import java.util.List; import java.util.Random; import org.comixedproject.adaptors.AdaptorException; -import org.comixedproject.model.comicfiles.ComicFile; +import org.comixedproject.model.comicfiles.ComicFileGroup; import org.comixedproject.model.net.GetAllComicsUnderRequest; import org.comixedproject.model.net.ImportComicFilesRequest; import org.comixedproject.model.net.comicfiles.FilenameMetadataRequest; @@ -66,7 +66,7 @@ public class ComicFileControllerTest { @Mock private FilenameScrapingRuleService filenameScrapingRuleService; @Mock private Job addComicsToLibraryJob; @Mock private JobLauncher jobLauncher; - @Mock private List comicFileList; + @Mock private List comicFileGroupList; @Mock private List filenameList; @Mock private JobExecution jobExecution; @Mock private FilenameMetadata filenameMetadata; @@ -101,13 +101,13 @@ public void testGetImportFileCover() throws AdaptorException { @Test public void testGetAllComicsUnderNoLimit() throws IOException { Mockito.when(comicFileService.getAllComicsUnder(Mockito.anyString(), Mockito.anyInt())) - .thenReturn(comicFileList); + .thenReturn(comicFileGroupList); final LoadComicFilesResponse response = controller.loadComicFiles(new GetAllComicsUnderRequest(TEST_DIRECTORY, TEST_NO_LIMIT)); assertNotNull(response); - assertSame(comicFileList, response.getFiles()); + assertSame(comicFileGroupList, response.getGroups()); Mockito.verify(comicFileService, Mockito.times(1)) .getAllComicsUnder(TEST_DIRECTORY, TEST_NO_LIMIT); @@ -116,13 +116,13 @@ public void testGetAllComicsUnderNoLimit() throws IOException { @Test public void testGetAllComicsUnder() throws IOException { Mockito.when(comicFileService.getAllComicsUnder(Mockito.anyString(), Mockito.anyInt())) - .thenReturn(comicFileList); + .thenReturn(comicFileGroupList); final LoadComicFilesResponse response = controller.loadComicFiles(new GetAllComicsUnderRequest(TEST_DIRECTORY, TEST_LIMIT)); assertNotNull(response); - assertSame(comicFileList, response.getFiles()); + assertSame(comicFileGroupList, response.getGroups()); Mockito.verify(comicFileService, Mockito.times(1)) .getAllComicsUnder(TEST_DIRECTORY, TEST_LIMIT); diff --git a/comixed-services/src/main/java/org/comixedproject/service/comicfiles/ComicFileService.java b/comixed-services/src/main/java/org/comixedproject/service/comicfiles/ComicFileService.java index 4fca54893..e09ea9359 100644 --- a/comixed-services/src/main/java/org/comixedproject/service/comicfiles/ComicFileService.java +++ b/comixed-services/src/main/java/org/comixedproject/service/comicfiles/ComicFileService.java @@ -22,15 +22,18 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FilenameUtils; import org.comixedproject.adaptors.AdaptorException; import org.comixedproject.adaptors.comicbooks.ComicBookAdaptor; import org.comixedproject.adaptors.comicbooks.ComicFileAdaptor; import org.comixedproject.model.comicbooks.Comic; import org.comixedproject.model.comicfiles.ComicFile; import org.comixedproject.model.comicfiles.ComicFileDescriptor; -import org.comixedproject.repositories.comicbooks.ComicRepository; +import org.comixedproject.model.comicfiles.ComicFileGroup; import org.comixedproject.repositories.comicfiles.ComicFileDescriptorRepository; +import org.comixedproject.service.comicbooks.ComicService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -45,7 +48,7 @@ @Log4j2 public class ComicFileService { @Autowired private ComicBookAdaptor comicBookAdaptor; - @Autowired private ComicRepository comicRepository; + @Autowired private ComicService comicService; @Autowired private ComicFileDescriptorRepository comicFileDescriptorRepository; @Autowired private ComicFileAdaptor comicFileAdaptor; @@ -54,12 +57,22 @@ public byte[] getImportFileCover(final String comicArchive) throws AdaptorExcept return this.comicBookAdaptor.loadCover(comicArchive); } - public List getAllComicsUnder(final String rootDirectory, final Integer maximum) + /** + * Retrieves up to a maximum number of comic files below a given root directory, groups by + * absolute directory. Returns only files that have a comic extension and which do not already + * appear in the database. + * + * @param rootDirectory the root directory + * @param maximum the maximum number of files + * @return the comic files + * @throws IOException if an error occurs + */ + public List getAllComicsUnder(final String rootDirectory, final int maximum) throws IOException { - log.debug("Getting comics below root: {}", rootDirectory); + log.debug("Getting {} comics below root: {}", maximum == 0 ? "all" : maximum, rootDirectory); final File rootFile = new File(rootDirectory); - final List result = new ArrayList<>(); + final List result = new ArrayList<>(); if (rootFile.exists()) { if (rootFile.isDirectory()) { @@ -75,24 +88,44 @@ public List getAllComicsUnder(final String rootDirectory, final Integ } private void loadFilesUnder( - final List files, final File directory, final Integer maximum) throws IOException { - log.debug("Loading files in directory: {}", directory); + final List entries, final File directory, final int maximum) + throws IOException { + log.trace("Loading files in directory: {}", directory); if (directory.listFiles() != null) { for (File file : directory.listFiles()) { - if (maximum > 0 && files.size() == maximum) { - log.debug("Loading maximum comics: {}", maximum); - return; + if (!entries.isEmpty()) { + final int total = + entries.stream() + .map(comicFileGroup -> comicFileGroup.getFiles().size()) + .reduce((runningTotal, entry) -> runningTotal += entry) + .get(); + if (maximum > 0 && total == maximum) { + log.trace("Finished loading comics"); + return; + } } if (file.isDirectory()) { - this.loadFilesUnder(files, file, maximum); + this.loadFilesUnder(entries, file, maximum); } else { if (canBeImported(file)) { final String filePath = file.getCanonicalPath(); final long fileSize = file.length(); - log.debug("Adding file: {} ({} bytes)", file.getAbsolutePath(), file.length()); - - files.add(new ComicFile(filePath, fileSize)); + final String parentPath = FilenameUtils.getPath(filePath); + final Optional entry = + entries.stream() + .filter(comicFileGroup -> comicFileGroup.getDirectory().equals(parentPath)) + .findFirst(); + ComicFileGroup group = null; + if (entry.isPresent()) { + group = entry.get(); + } else { + log.trace("Creating new grouping"); + group = new ComicFileGroup(parentPath); + entries.add(group); + } + log.trace("Adding comic file"); + group.getFiles().add(new ComicFile(filePath, fileSize)); } } } @@ -103,7 +136,8 @@ private boolean canBeImported(final File file) throws IOException { boolean isComic = this.comicFileAdaptor.isComicFile(file); final String filePath = file.getCanonicalPath(); - final Comic comic = this.comicRepository.findByFilename(filePath); + log.trace("Checking if comic file is already in the database"); + final Comic comic = this.comicService.findByFilename(filePath); return isComic && (comic == null); } diff --git a/comixed-services/src/test/java/org/comixedproject/service/comicfiles/ComicFileServiceTest.java b/comixed-services/src/test/java/org/comixedproject/service/comicfiles/ComicFileServiceTest.java index d71006be4..13ca32e22 100644 --- a/comixed-services/src/test/java/org/comixedproject/service/comicfiles/ComicFileServiceTest.java +++ b/comixed-services/src/test/java/org/comixedproject/service/comicfiles/ComicFileServiceTest.java @@ -28,10 +28,10 @@ import org.comixedproject.adaptors.comicbooks.ComicBookAdaptor; import org.comixedproject.adaptors.comicbooks.ComicFileAdaptor; import org.comixedproject.model.comicbooks.Comic; -import org.comixedproject.model.comicfiles.ComicFile; import org.comixedproject.model.comicfiles.ComicFileDescriptor; -import org.comixedproject.repositories.comicbooks.ComicRepository; +import org.comixedproject.model.comicfiles.ComicFileGroup; import org.comixedproject.repositories.comicfiles.ComicFileDescriptorRepository; +import org.comixedproject.service.comicbooks.ComicService; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -48,17 +48,18 @@ public class ComicFileServiceTest { private static final String TEST_ROOT_DIRECTORY = "src/test/resources"; private static final String TEST_COMIC_ARCHIVE = TEST_ROOT_DIRECTORY + "/" + TEST_ARCHIVE_FILENAME; - private static final Integer TEST_LIMIT = 2; - private static final Integer TEST_NO_LIMIT = -1; + private static final int TEST_LIMIT = 2; + private static final int TEST_NO_LIMIT = -1; @InjectMocks private ComicFileService service; @Mock private ComicBookAdaptor comicBookAdaptor; @Mock private ComicFileAdaptor comicFileAdaptor; - @Mock private ComicRepository comicRepository; + @Mock private ComicService comicService; @Mock private ComicFileDescriptorRepository comicFileDescriptorRepository; @Mock private Comic comic; @Mock private ComicFileDescriptor savedComicFileDescriptor; @Mock private List comicFileDescriptors; + @Mock private ComicFileDescriptor comicFileDescriptor; @Test public void testGetImportFileCoverWithNoCover() throws AdaptorException { @@ -83,7 +84,7 @@ public void testGetImportFileCover() throws AdaptorException { @Test public void testGetAllComicsUnderInvalidDirectory() throws IOException { - final List result = + final List result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY + "/nonexistent", TEST_LIMIT); assertNotNull(result); @@ -92,7 +93,7 @@ public void testGetAllComicsUnderInvalidDirectory() throws IOException { @Test public void testGetAllComicsUnderWithFileSupplied() throws IOException { - final List result = service.getAllComicsUnder(TEST_COMIC_ARCHIVE, TEST_LIMIT); + final List result = service.getAllComicsUnder(TEST_COMIC_ARCHIVE, TEST_LIMIT); assertNotNull(result); assertTrue(result.isEmpty()); @@ -101,14 +102,14 @@ public void testGetAllComicsUnderWithFileSupplied() throws IOException { @Test public void testGetAllComicsAlreadyImported() throws IOException { Mockito.when(comicFileAdaptor.isComicFile(Mockito.any(File.class))).thenReturn(true); - Mockito.when(comicRepository.findByFilename(Mockito.anyString())).thenReturn(comic); + Mockito.when(comicService.findByFilename(Mockito.anyString())).thenReturn(comic); - final List result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT); + final List result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT); assertNotNull(result); assertTrue(result.isEmpty()); - Mockito.verify(comicRepository, Mockito.times(1)) + Mockito.verify(comicService, Mockito.times(1)) .findByFilename(new File(TEST_COMIC_ARCHIVE).getCanonicalPath()); } @@ -116,22 +117,30 @@ public void testGetAllComicsAlreadyImported() throws IOException { public void testGetAllComicsUnderWithLimit() throws IOException { Mockito.when(comicFileAdaptor.isComicFile(Mockito.any(File.class))).thenReturn(true); - final List result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT); + final List result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_LIMIT); assertNotNull(result); assertFalse(result.isEmpty()); - assertEquals(TEST_LIMIT.intValue(), result.size()); + assertEquals( + TEST_LIMIT, + result.stream() + .map(comicFileGroup -> comicFileGroup.getFiles().size()) + .reduce((sum, size) -> sum += size) + .get() + .intValue()); } @Test public void testGetAllComicsUnder() throws IOException { Mockito.when(comicFileAdaptor.isComicFile(Mockito.any(File.class))).thenCallRealMethod(); - final List result = service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_NO_LIMIT); + final List result = + service.getAllComicsUnder(TEST_ROOT_DIRECTORY, TEST_NO_LIMIT); assertNotNull(result); assertFalse(result.isEmpty()); - assertEquals(3, result.size()); + assertEquals(1, result.size()); + assertEquals(3, result.get(0).getFiles().size()); } @Test @@ -163,4 +172,11 @@ public void testFindComicFileDescriptors() { Mockito.verify(comicFileDescriptorRepository, Mockito.times(1)).findAll(); } + + @Test + public void testDeleteComicFileDescriptor() { + service.deleteComicFileDescriptor(comicFileDescriptor); + + Mockito.verify(comicFileDescriptorRepository, Mockito.times(1)).delete(comicFileDescriptor); + } } diff --git a/comixed-webui/src/app/comic-files/actions/comic-file-list.actions.ts b/comixed-webui/src/app/comic-files/actions/comic-file-list.actions.ts index 174ff4f93..04ecb9658 100644 --- a/comixed-webui/src/app/comic-files/actions/comic-file-list.actions.ts +++ b/comixed-webui/src/app/comic-files/actions/comic-file-list.actions.ts @@ -18,6 +18,7 @@ import { createAction, props } from '@ngrx/store'; import { ComicFile } from '@app/comic-files/models/comic-file'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; export const loadComicFiles = createAction( '[Comic File List] Load comics in a file system', @@ -26,7 +27,7 @@ export const loadComicFiles = createAction( export const comicFilesLoaded = createAction( '[Comic File List] Loaded comics in a file system', - props<{ files: ComicFile[] }>() + props<{ groups: ComicFileGroup[] }>() ); export const loadComicFilesFailed = createAction( diff --git a/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.spec.ts b/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.spec.ts index c002d1ac6..d694c2390 100644 --- a/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.spec.ts +++ b/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.spec.ts @@ -46,9 +46,19 @@ import { } from '@app/library/library.constants'; import { hot } from 'jasmine-marbles'; import { HttpErrorResponse } from '@angular/common/http'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; describe('ComicFileListEffects', () => { - const FILES = [COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3, COMIC_FILE_4]; + const GROUPS: ComicFileGroup[] = [ + { + directory: 'directory1', + files: [COMIC_FILE_1, COMIC_FILE_3] + }, + { + directory: 'directory2', + files: [COMIC_FILE_2] + } + ]; let actions$: Observable; let effects: ComicFileListEffects; @@ -96,12 +106,12 @@ describe('ComicFileListEffects', () => { const MAXIMUM_RESULT = 100; it('fires an action on success', () => { - const serviceResponse = { files: FILES } as LoadComicFilesResponse; + const serviceResponse = { groups: GROUPS } as LoadComicFilesResponse; const action = loadComicFiles({ directory: ROOT_DIRECTORY, maximum: MAXIMUM_RESULT }); - const outcome1 = comicFilesLoaded({ files: FILES }); + const outcome1 = comicFilesLoaded({ groups: GROUPS }); const outcome2 = saveUserPreference({ name: IMPORT_ROOT_DIRECTORY_PREFERENCE, value: ROOT_DIRECTORY diff --git a/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.ts b/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.ts index 3cfbdbe3b..330875502 100644 --- a/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.ts +++ b/comixed-webui/src/app/comic-files/effects/comic-file-list.effects.ts @@ -54,12 +54,20 @@ export class ComicFileListEffects { this.alertService.info( this.translateService.instant( 'comic-files.load-comic-files.effect-success', - { count: response.files.length } + { + directories: response.groups.length, + files: response.groups + .map(group => group.files) + .reduce( + (accumulator, files) => accumulator.concat(files), + [] + ).length + } ) ) ), mergeMap((response: LoadComicFilesResponse) => [ - comicFilesLoaded({ files: response.files }), + comicFilesLoaded({ groups: response.groups }), saveUserPreference({ name: IMPORT_ROOT_DIRECTORY_PREFERENCE, value: action.directory diff --git a/comixed-webui/src/app/comic-files/models/comic-file-group.ts b/comixed-webui/src/app/comic-files/models/comic-file-group.ts new file mode 100644 index 000000000..37f39ce3c --- /dev/null +++ b/comixed-webui/src/app/comic-files/models/comic-file-group.ts @@ -0,0 +1,24 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2021, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { ComicFile } from './comic-file'; + +export interface ComicFileGroup { + directory: string; + files: ComicFile[]; +} diff --git a/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.spec.ts b/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.spec.ts index 9c4f3af62..91caceed8 100644 --- a/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.spec.ts +++ b/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.spec.ts @@ -34,8 +34,19 @@ import { loadComicFilesFailed, setComicFilesSelectedState } from '@app/comic-files/actions/comic-file-list.actions'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; describe('ComicFileList Reducer', () => { + const GROUPS: ComicFileGroup[] = [ + { + directory: 'directory1', + files: [COMIC_FILE_1, COMIC_FILE_3] + }, + { + directory: 'directory2', + files: [COMIC_FILE_2] + } + ]; const FILES = [COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3]; let state: ComicFileListState; @@ -78,8 +89,8 @@ describe('ComicFileList Reducer', () => { describe('comic files received', () => { beforeEach(() => { state = reducer( - { ...state, loading: true, files: [], selections: FILES }, - comicFilesLoaded({ files: FILES }) + { ...state, loading: true, groups: [], files: [], selections: GROUPS }, + comicFilesLoaded({ groups: GROUPS }) ); }); @@ -87,8 +98,12 @@ describe('ComicFileList Reducer', () => { expect(state.loading).toBeFalse(); }); + it('sets the comic file groups', () => { + expect(state.groups).toEqual(GROUPS); + }); + it('sets the comic files', () => { - expect(state.files).toEqual(FILES); + expect(state.files).toEqual([COMIC_FILE_1, COMIC_FILE_3, COMIC_FILE_2]); }); it('clears any previous selections', () => { @@ -99,7 +114,7 @@ describe('ComicFileList Reducer', () => { describe('failure to load comic files', () => { beforeEach(() => { state = reducer( - { ...state, loading: true, files: FILES, selections: FILES }, + { ...state, loading: true, files: GROUPS, selections: GROUPS }, loadComicFilesFailed() ); }); @@ -120,7 +135,7 @@ describe('ComicFileList Reducer', () => { describe('selecting comic files', () => { beforeEach(() => { state = reducer( - { ...state, selections: [FILES[0]] }, + { ...state, selections: [GROUPS[0]] }, setComicFilesSelectedState({ files: FILES, selected: true }) ); }); @@ -131,11 +146,11 @@ describe('ComicFileList Reducer', () => { }); describe('deselecting comic files', () => { - const DESELECTED_FILE = FILES[1]; + const DESELECTED_FILE = FILES[0]; beforeEach(() => { state = reducer( - { ...state, selections: FILES }, + { ...state, selections: GROUPS }, setComicFilesSelectedState({ files: [DESELECTED_FILE], selected: false @@ -151,7 +166,7 @@ describe('ComicFileList Reducer', () => { describe('clearing the selections', () => { beforeEach(() => { state = reducer( - { ...state, selections: FILES }, + { ...state, selections: GROUPS }, clearComicFileSelections() ); }); diff --git a/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.ts b/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.ts index 994343186..f80c1e824 100644 --- a/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.ts +++ b/comixed-webui/src/app/comic-files/reducers/comic-file-list.reducer.ts @@ -25,17 +25,20 @@ import { loadComicFilesFailed, setComicFilesSelectedState } from '@app/comic-files/actions/comic-file-list.actions'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; export const COMIC_FILE_LIST_FEATURE_KEY = 'comic_file_list'; export interface ComicFileListState { loading: boolean; + groups: ComicFileGroup[]; files: ComicFile[]; selections: ComicFile[]; } export const initialState: ComicFileListState = { loading: false, + groups: [], files: [], selections: [] }; @@ -44,12 +47,19 @@ export const reducer = createReducer( initialState, on(loadComicFiles, state => ({ ...state, loading: true })), - on(comicFilesLoaded, (state, action) => ({ - ...state, - loading: false, - files: action.files, - selections: [] - })), + on(comicFilesLoaded, (state, action) => { + const groups = action.groups; + const files = action.groups + .map(entry => entry.files) + .reduce((accumulator, entries) => accumulator.concat(entries), []); + return { + ...state, + loading: false, + groups, + files, + selections: [] + }; + }), on(loadComicFilesFailed, state => ({ ...state, loading: false, diff --git a/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.spec.ts b/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.spec.ts index 37c5fc84f..f6f6ef1ef 100644 --- a/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.spec.ts +++ b/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.spec.ts @@ -21,6 +21,7 @@ import { ComicFileListState } from '../reducers/comic-file-list.reducer'; import { + selectComicFileGroups, selectComicFileListState, selectComicFiles, selectComicFileSelections @@ -30,8 +31,19 @@ import { COMIC_FILE_2, COMIC_FILE_3 } from '@app/comic-files/comic-file.fixtures'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; describe('ComicFileList Selectors', () => { + const GROUPS: ComicFileGroup[] = [ + { + directory: 'directory1', + files: [COMIC_FILE_1, COMIC_FILE_3] + }, + { + directory: 'directory2', + files: [COMIC_FILE_2] + } + ]; const FILES = [COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3]; let state: ComicFileListState; @@ -39,6 +51,7 @@ describe('ComicFileList Selectors', () => { beforeEach(() => { state = { loading: Math.random() > 0.5, + groups: GROUPS, files: FILES, selections: FILES }; @@ -52,6 +65,14 @@ describe('ComicFileList Selectors', () => { ).toEqual(state); }); + it('should select the comic groups', () => { + expect( + selectComicFileGroups({ + [COMIC_FILE_LIST_FEATURE_KEY]: state + }) + ).toEqual(state.groups); + }); + it('should select the comic files', () => { expect( selectComicFiles({ diff --git a/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.ts b/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.ts index 74914719e..d7ac0c654 100644 --- a/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.ts +++ b/comixed-webui/src/app/comic-files/selectors/comic-file-list.selectors.ts @@ -21,12 +21,17 @@ import { COMIC_FILE_LIST_FEATURE_KEY, ComicFileListState } from '@app/comic-files/reducers/comic-file-list.reducer'; -import { selectComicImportState } from '@app/comic-files/selectors/comic-import.selectors'; /** Selects the comic list feature state. */ export const selectComicFileListState = createFeatureSelector(COMIC_FILE_LIST_FEATURE_KEY); +/** Selects the loaded comic file groups. */ +export const selectComicFileGroups = createSelector( + selectComicFileListState, + state => state.groups +); + /** Selects the loaded comic files. */ export const selectComicFiles = createSelector( selectComicFileListState, diff --git a/comixed-webui/src/app/comic-files/services/comic-import.service.spec.ts b/comixed-webui/src/app/comic-files/services/comic-import.service.spec.ts index 572952758..9fa43f09d 100644 --- a/comixed-webui/src/app/comic-files/services/comic-import.service.spec.ts +++ b/comixed-webui/src/app/comic-files/services/comic-import.service.spec.ts @@ -32,7 +32,6 @@ import { COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3, - COMIC_FILE_4, ROOT_DIRECTORY } from '@app/comic-files/comic-file.fixtures'; import { @@ -43,9 +42,20 @@ import { import { COMIC_2 } from '@app/comic-books/comic-books.fixtures'; import { FilenameMetadataResponse } from '@app/comic-files/models/net/filename-metadata-response'; import { FilenameMetadataRequest } from '@app/comic-files/models/net/filename-metadata-request'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; describe('ComicImportService', () => { - const FILES = [COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3, COMIC_FILE_4]; + const GROUPS: ComicFileGroup[] = [ + { + directory: 'directory1', + files: [COMIC_FILE_1, COMIC_FILE_3] + }, + { + directory: 'directory2', + files: [COMIC_FILE_2] + } + ]; + const FILES = [COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3]; const MAXIMUM = 100; const FILENAME = COMIC_2.baseFilename; const SERIES = COMIC_2.series; @@ -71,7 +81,7 @@ describe('ComicImportService', () => { it('can load comic files', () => { const serviceResponse = { - files: FILES + groups: GROUPS } as LoadComicFilesResponse; service .loadComicFiles({ directory: ROOT_DIRECTORY, maximum: MAXIMUM }) diff --git a/comixed-webui/src/app/library/models/net/load-comic-files-response.ts b/comixed-webui/src/app/library/models/net/load-comic-files-response.ts index f956377e8..ad95ad256 100644 --- a/comixed-webui/src/app/library/models/net/load-comic-files-response.ts +++ b/comixed-webui/src/app/library/models/net/load-comic-files-response.ts @@ -16,8 +16,8 @@ * along with this program. If not, see */ -import { ComicFile } from '../../../comic-files/models/comic-file'; +import { ComicFileGroup } from '@app/comic-files/models/comic-file-group'; export interface LoadComicFilesResponse { - files: ComicFile[]; + groups: ComicFileGroup[]; } diff --git a/comixed-webui/src/app/library/pages/library-page/library-page.component.spec.ts b/comixed-webui/src/app/library/pages/library-page/library-page.component.spec.ts index 8edc8b738..bbc18c358 100644 --- a/comixed-webui/src/app/library/pages/library-page/library-page.component.spec.ts +++ b/comixed-webui/src/app/library/pages/library-page/library-page.component.spec.ts @@ -166,7 +166,9 @@ describe('LibraryPageComponent', () => { describe('when showing unprocessed comics', () => { beforeEach(() => { - (activatedRoute.data as BehaviorSubject<{}>).next({ unprocessed: true }); + (activatedRoute.data as BehaviorSubject<{}>).next({ + unprocessed: true + }); }); it('sets the unread only flag', () => { diff --git a/comixed-webui/src/assets/i18n/de/comic-files.json b/comixed-webui/src/assets/i18n/de/comic-files.json index c63add02e..97798ac3b 100644 --- a/comixed-webui/src/assets/i18n/de/comic-files.json +++ b/comixed-webui/src/assets/i18n/de/comic-files.json @@ -16,7 +16,7 @@ "maximum-1000-files": "1000 files" }, "load-comic-files": { - "effect-success": "Found {count, plural, =1{one comic file} other{# comic files}}.", + "effect-success": "Found {files, plural, =1{one comic file} other{# comic files}} in {directories, plural, =1{one directory} other{# directories}}.", "effect-failure": "Failed to find any comic files." }, "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected", diff --git a/comixed-webui/src/assets/i18n/en/comic-files.json b/comixed-webui/src/assets/i18n/en/comic-files.json index c63add02e..97798ac3b 100644 --- a/comixed-webui/src/assets/i18n/en/comic-files.json +++ b/comixed-webui/src/assets/i18n/en/comic-files.json @@ -16,7 +16,7 @@ "maximum-1000-files": "1000 files" }, "load-comic-files": { - "effect-success": "Found {count, plural, =1{one comic file} other{# comic files}}.", + "effect-success": "Found {files, plural, =1{one comic file} other{# comic files}} in {directories, plural, =1{one directory} other{# directories}}.", "effect-failure": "Failed to find any comic files." }, "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected", diff --git a/comixed-webui/src/assets/i18n/es/comic-files.json b/comixed-webui/src/assets/i18n/es/comic-files.json index f162e6830..e4c5c68f9 100644 --- a/comixed-webui/src/assets/i18n/es/comic-files.json +++ b/comixed-webui/src/assets/i18n/es/comic-files.json @@ -16,7 +16,7 @@ "maximum-1000-files": "1000 archivos" }, "load-comic-files": { - "effect-success": "Encontró {count, plural, =1{one comic file} other{# comic files}}.", + "effect-success": "Found {files, plural, =1{one comic file} other{# comic files}} in {directories, plural, =1{one directory} other{# directories}}.", "effect-failure": "No se pudo encontrar ningún archivo de cómic." }, "page-title": "Importar cómics: {selected, plural, =1{One} other{#}} de {count, plural, =1{One Comic} other{# Comics}} seleccionado", diff --git a/comixed-webui/src/assets/i18n/fr/comic-files.json b/comixed-webui/src/assets/i18n/fr/comic-files.json index 2d62656f6..c181e9ea6 100644 --- a/comixed-webui/src/assets/i18n/fr/comic-files.json +++ b/comixed-webui/src/assets/i18n/fr/comic-files.json @@ -16,7 +16,7 @@ "maximum-1000-files": "1000 fichiers" }, "load-comic-files": { - "effect-success": "{count, plural, =1{1 fichier de bande dessinée trouvé} other{# fichiers de bande dessinée trouvés}}.", + "effect-success": "Found {files, plural, =1{one comic file} other{# comic files}} in {directories, plural, =1{one directory} other{# directories}}.", "effect-failure": "Impossible de trouver des fichiers de Bande Dessinées pour importation." }, "page-title": "Import de Bandes Dessinées: {selected, plural, =1{Une} other{#}} sur {count, plural, =1{une Bande dessinée sélectionnée} other{# Bandes Dessinées sélectionnées}}", diff --git a/comixed-webui/src/assets/i18n/pt/comic-files.json b/comixed-webui/src/assets/i18n/pt/comic-files.json index c63add02e..97798ac3b 100644 --- a/comixed-webui/src/assets/i18n/pt/comic-files.json +++ b/comixed-webui/src/assets/i18n/pt/comic-files.json @@ -16,7 +16,7 @@ "maximum-1000-files": "1000 files" }, "load-comic-files": { - "effect-success": "Found {count, plural, =1{one comic file} other{# comic files}}.", + "effect-success": "Found {files, plural, =1{one comic file} other{# comic files}} in {directories, plural, =1{one directory} other{# directories}}.", "effect-failure": "Failed to find any comic files." }, "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected",