show reading progress in author page. Allow deletion of tags, even if assigned to story.

This commit is contained in:
Stefan Hardegger
2025-10-31 09:54:04 +01:00
parent 7a4dd567dc
commit 75768855e2
5 changed files with 61 additions and 17 deletions

View File

@@ -691,6 +691,7 @@ public class StoryController {
// Reading progress fields // Reading progress fields
dto.setIsRead(story.getIsRead()); dto.setIsRead(story.getIsRead());
dto.setReadingPosition(story.getReadingPosition()); dto.setReadingPosition(story.getReadingPosition());
dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
dto.setLastReadAt(story.getLastReadAt()); dto.setLastReadAt(story.getLastReadAt());
if (story.getAuthor() != null) { if (story.getAuthor() != null) {

View File

@@ -23,6 +23,7 @@ public class StorySummaryDto {
// Reading progress fields // Reading progress fields
private Boolean isRead; private Boolean isRead;
private Integer readingPosition; private Integer readingPosition;
private Integer readingProgressPercentage; // Pre-calculated percentage (0-100)
private LocalDateTime lastReadAt; private LocalDateTime lastReadAt;
// Related entities as simple references // Related entities as simple references
@@ -127,6 +128,14 @@ public class StorySummaryDto {
this.readingPosition = readingPosition; this.readingPosition = readingPosition;
} }
public Integer getReadingProgressPercentage() {
return readingProgressPercentage;
}
public void setReadingProgressPercentage(Integer readingProgressPercentage) {
this.readingProgressPercentage = readingProgressPercentage;
}
public LocalDateTime getLastReadAt() { public LocalDateTime getLastReadAt() {
return lastReadAt; return lastReadAt;
} }

View File

@@ -33,6 +33,7 @@ public class TagService {
private final TagRepository tagRepository; private final TagRepository tagRepository;
private final TagAliasRepository tagAliasRepository; private final TagAliasRepository tagAliasRepository;
private SolrService solrService;
@Autowired @Autowired
public TagService(TagRepository tagRepository, TagAliasRepository tagAliasRepository) { public TagService(TagRepository tagRepository, TagAliasRepository tagAliasRepository) {
@@ -40,6 +41,11 @@ public class TagService {
this.tagAliasRepository = tagAliasRepository; this.tagAliasRepository = tagAliasRepository;
} }
@Autowired(required = false)
public void setSolrService(SolrService solrService) {
this.solrService = solrService;
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<Tag> findAll() { public List<Tag> findAll() {
return tagRepository.findAll(); return tagRepository.findAll();
@@ -143,12 +149,38 @@ public class TagService {
public void delete(UUID id) { public void delete(UUID id) {
Tag tag = findById(id); Tag tag = findById(id);
// Check if tag is used by any stories // Remove tag from all stories before deletion and track for reindexing
List<Story> storiesToReindex = new ArrayList<>();
if (!tag.getStories().isEmpty()) { if (!tag.getStories().isEmpty()) {
throw new IllegalStateException("Cannot delete tag that is used by stories. Remove tag from all stories first."); // Create a copy to avoid ConcurrentModificationException
List<Story> storiesToUpdate = new ArrayList<>(tag.getStories());
storiesToUpdate.forEach(story -> {
story.removeTag(tag);
storiesToReindex.add(story);
});
logger.info("Removed tag '{}' from {} stories before deletion", tag.getName(), storiesToUpdate.size());
}
// Remove tag from all collections before deletion
if (tag.getCollections() != null && !tag.getCollections().isEmpty()) {
tag.getCollections().forEach(collection -> collection.getTags().remove(tag));
logger.info("Removed tag '{}' from {} collections before deletion", tag.getName(), tag.getCollections().size());
} }
tagRepository.delete(tag); tagRepository.delete(tag);
logger.info("Deleted tag '{}'", tag.getName());
// Reindex affected stories in Solr
if (solrService != null && !storiesToReindex.isEmpty()) {
try {
for (Story story : storiesToReindex) {
solrService.indexStory(story);
}
logger.info("Reindexed {} stories after tag deletion", storiesToReindex.size());
} catch (Exception e) {
logger.error("Failed to reindex stories after tag deletion", e);
}
}
} }
public List<Tag> deleteUnusedTags() { public List<Tag> deleteUnusedTags() {

View File

@@ -137,9 +137,10 @@ export default function TagMaintenancePage() {
// Reload tags and reset selection // Reload tags and reset selection
await loadTags(); await loadTags();
setSelectedTagIds(new Set()); setSelectedTagIds(new Set());
} catch (error) { } catch (error: any) {
console.error('Failed to delete tags:', error); console.error('Failed to delete tags:', error);
alert('Failed to delete some tags. Please try again.'); const errorMessage = error.response?.data?.error || error.message || 'Failed to delete some tags. Please try again.';
alert(errorMessage);
} }
}; };

View File

@@ -129,7 +129,8 @@ export default function TagEditModal({ tag, isOpen, onClose, onSave, onDelete }:
onDelete(tag); onDelete(tag);
onClose(); onClose();
} catch (error: any) { } catch (error: any) {
setErrors({ submit: error.message }); const errorMessage = error.response?.data?.error || error.message || 'Failed to delete tag';
setErrors({ submit: errorMessage });
} finally { } finally {
setSaving(false); setSaving(false);
} }