Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ border-radius: 128px;

- Features
- Series years, status and summary in the metadata pane.
- Browser title shows series years and a Tags button.
- Tag editor edits the MetronInfo series start year.

## v2.5.5
Expand Down
2 changes: 2 additions & 0 deletions codex/serializers/browser/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ class BrowserTitleSerializer(Serializer):
collection_name = CharField(read_only=True)
collection_number_to = CharField(read_only=True)
collection_count = IntegerField(read_only=True, allow_null=True)
collection_start_year = IntegerField(read_only=True, allow_null=True)
collection_end_year = IntegerField(read_only=True, allow_null=True)


def _emit_cover(row, instance) -> None:
Expand Down
51 changes: 29 additions & 22 deletions codex/views/browser/title.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
"""Browser title."""

from collections.abc import Mapping
from types import MappingProxyType
from typing import Final

from codex.models import Comic, Volume
from codex.models import Comic, Series, Volume
from codex.views.browser.breadcrumbs import BrowserBreadcrumbsView

_EMPTY_TITLE: Final = MappingProxyType(
{
"collection_name": "",
"collection_number_to": None,
"collection_count": 0,
"collection_start_year": None,
"collection_end_year": None,
}
)


class BrowserTitleView(BrowserBreadcrumbsView):
"""Browser title methods."""
Expand All @@ -19,7 +31,7 @@ def _get_root_collection_name(self) -> tuple:
raise ValueError(reason)
return plural.capitalize(), 0

def _get_collection_name(self) -> tuple:
def _get_collection_name(self) -> Mapping:
"""
Name the collection the route points at.

Expand All @@ -29,32 +41,27 @@ def _get_collection_name(self) -> tuple:
collection this user cannot see, so the empty defaults below are
now only the root-listing case (no pks).
"""
collection_number_to = None
collection_count = 0
collection_name = ""
title = dict(_EMPTY_TITLE)
if gi := self.collection_instance:
collection_name = gi.name
if isinstance(gi, Volume):
collection_number_to = gi.number_to
collection_count = gi.series.volume_count
title["collection_name"] = gi.name
if isinstance(gi, Series):
title["collection_start_year"] = gi.start_year
title["collection_end_year"] = gi.end_year
elif isinstance(gi, Volume):
title["collection_number_to"] = gi.number_to
title["collection_count"] = gi.series.volume_count
elif isinstance(gi, Comic):
collection_number_to = gi.volume.number_to
collection_count = gi.volume.issue_count
return collection_name, collection_number_to, collection_count
title["collection_number_to"] = gi.volume.number_to
title["collection_count"] = gi.volume.issue_count
return title

def get_browser_page_title(self) -> Mapping:
"""Get the proper title for this browse level."""
pks = self.kwargs.get("pks")
if not pks:
collection_name, collection_count = self._get_root_collection_name()
collection_number_to = None
else:
collection_name, collection_number_to, collection_count = (
self._get_collection_name()
)

if self.kwargs.get("pks"):
return self._get_collection_name()
collection_name, collection_count = self._get_root_collection_name()
return {
**_EMPTY_TITLE,
"collection_name": collection_name,
"collection_number_to": collection_number_to,
"collection_count": collection_count,
}
9 changes: 9 additions & 0 deletions frontend/src/comic-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,17 @@ export const getFullComicName = function (
return [seriesName, fvn, issueName].filter(Boolean).join(" ");
};

export const formattedYearRange = function (start, end) {
// A series run: "1963–1996", open-ended "1963–", "–1996", or one year.
if (!start && !end) {
return "";
}
return start === end ? String(start) : `${start || ""}–${end || ""}`;
};

export default {
getFullComicName,
formattedVolumeName,
formattedYearRange,
getIssueName,
};
58 changes: 53 additions & 5 deletions frontend/src/components/browser/toolbars/browser-toolbar-title.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
<v-toolbar-title id="browserTitle" class="browserTitle">
{{ title }}
</v-toolbar-title>
<div v-if="titleBook" class="titleTagButton">
<MetadataDialog
:book="titleBook"
toolbar
size="small"
density="compact"
/>
</div>
<template v-if="subtitle" #extension>
<v-toolbar-title id="browserSubtitle" class="browserTitle">
{{ subtitle }}
Expand All @@ -15,13 +23,17 @@
import { mdiReload } from "@mdi/js";
import { mapState } from "pinia";

import { formattedVolumeName } from "@/comic-name";
import { formattedVolumeName, formattedYearRange } from "@/comic-name";
import MetadataDialog from "@/components/metadata/metadata-dialog.vue";
import { collectionForRoute } from "@/route";
import { useAuthStore } from "@/stores/auth";
import { useBrowserStore } from "@/stores/browser";

export default {
name: "BrowserHeader",
components: {
MetadataDialog,
},
data() {
return {
mdiReload,
Expand All @@ -44,20 +56,37 @@ export default {
modelCollection: (state) => state.page.modelCollection,
}),
...mapState(useAuthStore, ["isUserAdmin"]),
routeCollection() {
routeTarget() {
return collectionForRoute({
collection: this.$route.params.collection,
parentIds: this.$route.params.parentIds,
}).collection;
});
},
routeCollection() {
return this.routeTarget.collection;
},
titleBook() {
// The collection being browsed, as a card-shaped book for its Tags button.
const { collection, pks } = this.routeTarget;
if (collection === "root" || pks.length === 0) {
return null;
}
const ids = pks.map(Number);
return { collection, ids, pk: ids[0], childCount: 0 };
},
title() {
let title;
if (!this.$route.params.parentIds) {
title = "All";
} else if (this.browserTitle) {
let names = [];
const { collectionName, collectionNumberTo, collectionCount } =
this.browserTitle;
const {
collectionName,
collectionNumberTo,
collectionCount,
collectionStartYear,
collectionEndYear,
} = this.browserTitle;
const collection = this.routeCollection;
const formattedCollectionName =
collection === "volumes"
Expand All @@ -66,6 +95,13 @@ export default {
if (formattedCollectionName) {
names.push(formattedCollectionName);
}
const years = formattedYearRange(
collectionStartYear,
collectionEndYear,
);
if (years) {
names.push(years);
}
if (collectionCount) {
const formattedCollectionCount = `of ${collectionCount}`;
names.push(formattedCollectionCount);
Expand All @@ -90,6 +126,16 @@ export default {
padding-bottom: 6px;
}

/* Out of the flex flow so the title stays centred on the whole bar. */
.titleTagButton {
position: absolute;
top: 0px;
right: 0px;
height: 24px;
display: flex;
align-items: center;
}

.browserTitle {
margin: 0px;
text-align: center;
Expand All @@ -98,6 +144,8 @@ export default {
}

#browserTitle {
/* Symmetric room for the Tags button keeps a long title centred. */
padding-inline: 24px;
font-size: clamp(12px, 2vw, 20px);
}

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/metadata/metadata-activator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
class="tagButton cardControlButton"
:variant="variant"
:icon="mdiTagOutline"
:size="size"
:density="density"
title="Tags"
@mouseenter="onMouseEnter"
@click.prevent
Expand All @@ -21,6 +23,8 @@ export default {
props: {
book: { type: Object, required: true },
toolbar: { type: Boolean, require: true },
size: { type: String, default: undefined },
density: { type: String, default: undefined },
},
data() {
return {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/metadata/metadata-body.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<MetadataText :value="md.summary" :max-height="100" />
<MetadataText
:value="md.seriesSummary"
label="Series"
label="Series Summary"
:max-height="100"
/>
<MetadataText :value="md.review" label="Review" :max-height="100" />
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/components/metadata/metadata-dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
v-bind="activatorProps"
:book="book"
:toolbar="toolbar"
:size="size"
:density="density"
/>
</template>
<CloseButton class="closeButton" title="Close Tags (esc)" @click="close" />
Expand Down Expand Up @@ -79,6 +81,14 @@ export default {
type: Boolean,
default: false,
},
size: {
type: String,
default: undefined,
},
density: {
type: String,
default: undefined,
},
},
emits: ["close"],
data() {
Expand Down
28 changes: 14 additions & 14 deletions frontend/src/components/metadata/metadata-header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
collection="series"
:highlight="'series' === md.collection"
/>
<MetadataText :value="seriesYears" class="subdued noWrap" />
<MetadataText :value="md.seriesStatus" class="subdued" />
<MetadataText
v-if="md.volumeList?.length === 1"
id="volume"
Expand All @@ -43,6 +41,9 @@
/>
<MetadataText :value="volumeIssueCount" class="subdued" />
</div>
<div v-if="seriesFacts" id="seriesFactsRow" class="subdued">
{{ seriesFacts }}
</div>
</section>
<span v-if="!multiSelect && md.name" id="titleRow">
{{ collectionTitle }} {{ md.name }}
Expand Down Expand Up @@ -125,7 +126,7 @@
import { mapState } from "pinia";
import prettyBytes from "pretty-bytes";

import { formattedIssue } from "@/comic-name";
import { formattedIssue, formattedYearRange } from "@/comic-name";
import MetadataControls from "@/components/metadata/metadata-controls.vue";
import MetadataBookCover from "@/components/metadata/metadata-cover.vue";
import MetadataTags from "@/components/metadata/metadata-tags.vue";
Expand Down Expand Up @@ -242,11 +243,12 @@ export default {
return this?.md?.fileType || "Unknown";
},
seriesYears() {
const start = this.md.seriesStartYear;
const end = this.md.seriesEndYear;
if (!start && !end) return "";
if (start === end) return `(${start})`;
return `(${start || ""}–${end || " "})`;
return formattedYearRange(this.md.seriesStartYear, this.md.seriesEndYear);
},
seriesFacts() {
return [this.seriesYears, this.md.seriesStatus]
.filter(Boolean)
.join(" · ");
},
seriesVolumeCount() {
const count = this.md.seriesVolumeCount;
Expand Down Expand Up @@ -304,18 +306,15 @@ export default {
padding-top: 0px;
}

#titleRow {
#titleRow,
#seriesFactsRow {
font-size: large;
}

.subdued {
color: rgb(var(--v-theme-text-disabled));
}

.noWrap {
white-space: nowrap;
}

#pageDateRow {
display: block;
font-size: smaller;
Expand All @@ -335,7 +334,8 @@ export default {
font-size: large;
}

#titleRow {
#titleRow,
#seriesFactsRow {
font-size: x-small;
}
}
Expand Down
Loading