chore: normalize line feeds

closes #243
This commit is contained in:
Gauthier Roebroeck 2020-07-21 10:19:16 +08:00
parent b69c4f62a2
commit 1c28a9496b
3 changed files with 502 additions and 501 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

View File

@ -1,204 +1,204 @@
<template> <template>
<v-item-group multiple v-model="selectedItems"> <v-item-group multiple v-model="selectedItems">
<div v-if="hasItems" <div v-if="hasItems"
ref="content" ref="content"
v-resize="onResize" v-resize="onResize"
> >
<draggable v-model="localItems" <draggable v-model="localItems"
:class="flexClass" :class="flexClass"
handle=".handle" handle=".handle"
v-bind="dragOptions" v-bind="dragOptions"
> >
<transition-group type="transition" <transition-group type="transition"
:name="!draggable ? 'flip-list' : null" :name="!draggable ? 'flip-list' : null"
:class="flexClass" :class="flexClass"
> >
<v-item <v-item
v-for="item in localItems" v-for="item in localItems"
:key="item.id" :key="item.id"
class="my-2 mx-2" class="my-2 mx-2"
v-slot:default="{ toggle, active }" :value="item" v-slot:default="{ toggle, active }" :value="item"
> >
<slot name="item"> <slot name="item">
<div style="position: relative" <div style="position: relative"
:class="draggable ? 'draggable-item' : undefined" :class="draggable ? 'draggable-item' : undefined"
> >
<item-card <item-card
class="item-card" class="item-card"
:item="item" :item="item"
:width="itemWidth" :width="itemWidth"
:selected="active" :selected="active"
:no-link="draggable || deletable" :no-link="draggable || deletable"
:preselect="shouldPreselect" :preselect="shouldPreselect"
:onEdit="(draggable || deletable) ? undefined : editFunction" :onEdit="(draggable || deletable) ? undefined : editFunction"
:onSelected="(draggable || deletable) ? undefined : selectable ? toggle: undefined" :onSelected="(draggable || deletable) ? undefined : selectable ? toggle: undefined"
:action-menu="actionMenu" :action-menu="actionMenu"
></item-card> ></item-card>
<v-slide-y-reverse-transition> <v-slide-y-reverse-transition>
<v-icon v-if="draggable" <v-icon v-if="draggable"
class="handle" class="handle"
style="position: absolute; bottom: 0; left: 50%; margin-left: -12px;" style="position: absolute; bottom: 0; left: 50%; margin-left: -12px;"
> >
mdi-drag-horizontal mdi-drag-horizontal
</v-icon> </v-icon>
</v-slide-y-reverse-transition> </v-slide-y-reverse-transition>
<!-- FAB delete (center) --> <!-- FAB delete (center) -->
<v-fab-transition> <v-fab-transition>
<v-btn <v-btn
v-if="deletable" v-if="deletable"
fab fab
small small
color="accent" color="accent"
class="fab-delete" class="fab-delete"
@click="deleteItem(item)" @click="deleteItem(item)"
style="position: absolute; bottom: 10px; right: 10px;" style="position: absolute; bottom: 10px; right: 10px;"
> >
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</v-btn> </v-btn>
</v-fab-transition> </v-fab-transition>
</div> </div>
</slot> </slot>
</v-item> </v-item>
</transition-group> </transition-group>
</draggable> </draggable>
</div> </div>
<v-row v-else justify="center"> <v-row v-else justify="center">
<slot name="empty"></slot> <slot name="empty"></slot>
</v-row> </v-row>
</v-item-group> </v-item-group>
</template> </template>
<script lang="ts"> <script lang="ts">
import ItemCard from '@/components/ItemCard.vue' import ItemCard from '@/components/ItemCard.vue'
import { computeCardWidth } from '@/functions/grid-utilities' import { computeCardWidth } from '@/functions/grid-utilities'
import Vue from 'vue' import Vue from 'vue'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
export default Vue.extend({ export default Vue.extend({
name: 'ItemBrowser', name: 'ItemBrowser',
components: { ItemCard, draggable }, components: { ItemCard, draggable },
props: { props: {
items: { items: {
type: Array, type: Array,
required: true, required: true,
}, },
fixedItemWidth: { fixedItemWidth: {
type: Number, type: Number,
required: false, required: false,
}, },
selectable: { selectable: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
selected: { selected: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
editFunction: { editFunction: {
type: Function, type: Function,
}, },
resizeFunction: { resizeFunction: {
type: Function, type: Function,
}, },
draggable: { draggable: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
deletable: { deletable: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
nowrap: { nowrap: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
actionMenu: { actionMenu: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, },
data: () => { data: () => {
return { return {
selectedItems: [], selectedItems: [],
localItems: [], localItems: [],
width: 150, width: 150,
} }
}, },
watch: { watch: {
selectedItems: { selectedItems: {
handler () { handler () {
this.$emit('update:selected', this.selectedItems) this.$emit('update:selected', this.selectedItems)
}, },
immediate: true, immediate: true,
}, },
selected: { selected: {
handler () { handler () {
this.selectedItems = this.selected as [] this.selectedItems = this.selected as []
}, },
immediate: true, immediate: true,
}, },
items: { items: {
handler () { handler () {
this.localItems = this.items as [] this.localItems = this.items as []
}, },
immediate: true, immediate: true,
}, },
localItems: { localItems: {
handler () { handler () {
this.$emit('update:items', this.localItems) this.$emit('update:items', this.localItems)
}, },
immediate: true, immediate: true,
}, },
}, },
computed: { computed: {
flexClass (): string { flexClass (): string {
return this.nowrap ? 'd-flex flex-nowrap' : 'd-flex flex-wrap' return this.nowrap ? 'd-flex flex-nowrap' : 'd-flex flex-wrap'
}, },
hasItems (): boolean { hasItems (): boolean {
return this.items.length > 0 return this.items.length > 0
}, },
itemWidth (): number { itemWidth (): number {
return this.fixedItemWidth ? this.fixedItemWidth : this.width return this.fixedItemWidth ? this.fixedItemWidth : this.width
}, },
shouldPreselect (): boolean { shouldPreselect (): boolean {
return this.selectedItems.length > 0 return this.selectedItems.length > 0
}, },
dragOptions (): any { dragOptions (): any {
return { return {
animation: 200, animation: 200,
group: 'item-cards', group: 'item-cards',
disabled: !this.draggable, disabled: !this.draggable,
ghostClass: 'ghost', ghostClass: 'ghost',
} }
}, },
}, },
methods: { methods: {
onResize () { onResize () {
const content = this.$refs.content as HTMLElement const content = this.$refs.content as HTMLElement
this.width = computeCardWidth(content.clientWidth, this.$vuetify.breakpoint.name) this.width = computeCardWidth(content.clientWidth, this.$vuetify.breakpoint.name)
}, },
deleteItem (item: any) { deleteItem (item: any) {
const index = this.localItems.findIndex((e: any) => e.id === item.id) const index = this.localItems.findIndex((e: any) => e.id === item.id)
this.localItems.splice(index, 1) this.localItems.splice(index, 1)
}, },
}, },
}) })
</script> </script>
<style scoped> <style scoped>
.ghost .item-card { .ghost .item-card {
opacity: 0.5; opacity: 0.5;
background: #c8ebfb; background: #c8ebfb;
} }
.handle { .handle {
cursor: grab; cursor: grab;
} }
.flip-list-move { .flip-list-move {
transition: transform 0.4s; transition: transform 0.4s;
} }
</style> </style>

View File

@ -1,297 +1,297 @@
<template> <template>
<v-hover :disabled="disableHover"> <v-hover :disabled="disableHover">
<template v-slot:default="{ hover }"> <template v-slot:default="{ hover }">
<v-card <v-card
:width="width" :width="width"
@click="onClick" @click="onClick"
:class="noLink ? 'no-link' : ''" :class="noLink ? 'no-link' : ''"
:ripple="false" :ripple="false"
> >
<!-- Thumbnail--> <!-- Thumbnail-->
<v-img <v-img
:src="thumbnailUrl" :src="thumbnailUrl"
lazy-src="../assets/cover.svg" lazy-src="../assets/cover.svg"
aspect-ratio="0.7071" aspect-ratio="0.7071"
contain contain
> >
<!-- unread tick for book --> <!-- unread tick for book -->
<div class="unread" v-if="isUnread"/> <div class="unread" v-if="isUnread"/>
<!-- unread count for series --> <!-- unread count for series -->
<span v-if="unreadCount" <span v-if="unreadCount"
class="white--text pa-1 px-2 subtitle-2" class="white--text pa-1 px-2 subtitle-2"
:style="{background: 'orange', position: 'absolute', right: 0}" :style="{background: 'orange', position: 'absolute', right: 0}"
> >
{{ unreadCount }} {{ unreadCount }}
</span> </span>
<!-- Thumbnail overlay --> <!-- Thumbnail overlay -->
<v-fade-transition> <v-fade-transition>
<v-overlay <v-overlay
v-if="hover || selected || preselect || actionMenuState" v-if="hover || selected || preselect || actionMenuState"
absolute absolute
:opacity="hover || actionMenuState ? 0.3 : 0" :opacity="hover || actionMenuState ? 0.3 : 0"
:class="`${hover || actionMenuState ? 'item-border-darken' : selected ? 'item-border' : 'item-border-transparent'} overlay-full`" :class="`${hover || actionMenuState ? 'item-border-darken' : selected ? 'item-border' : 'item-border-transparent'} overlay-full`"
> >
<!-- Circle icon for selection (top left) --> <!-- Circle icon for selection (top left) -->
<v-icon v-if="onSelected" <v-icon v-if="onSelected"
:color="selected ? 'secondary' : ''" :color="selected ? 'secondary' : ''"
style="position: absolute; top: 5px; left: 10px" style="position: absolute; top: 5px; left: 10px"
@click.stop="selectItem" @click.stop="selectItem"
> >
{{ selected || (preselect && hover) ? 'mdi-checkbox-marked-circle' : 'mdi-checkbox-blank-circle-outline' {{ selected || (preselect && hover) ? 'mdi-checkbox-marked-circle' : 'mdi-checkbox-blank-circle-outline'
}} }}
</v-icon> </v-icon>
<!-- FAB reading (center) --> <!-- FAB reading (center) -->
<v-btn <v-btn
v-if="bookReady && !selected && !preselect && canReadPages" v-if="bookReady && !selected && !preselect && canReadPages"
fab fab
x-large x-large
color="accent" color="accent"
style="position: absolute; top: 50%; left: 50%; margin-left: -36px; margin-top: -36px" style="position: absolute; top: 50%; left: 50%; margin-left: -36px; margin-top: -36px"
:to="{name: 'read-book', params: { bookId: item.id}}" :to="{name: 'read-book', params: { bookId: item.id}}"
> >
<v-icon>mdi-book-open-page-variant</v-icon> <v-icon>mdi-book-open-page-variant</v-icon>
</v-btn> </v-btn>
<!-- Pen icon for edition (bottom left) --> <!-- Pen icon for edition (bottom left) -->
<v-btn icon <v-btn icon
v-if="!selected && !preselect && onEdit" v-if="!selected && !preselect && onEdit"
style="position: absolute; bottom: 5px; left: 5px" style="position: absolute; bottom: 5px; left: 5px"
@click.stop="editItem" @click.stop="editItem"
> >
<v-icon>mdi-pencil</v-icon> <v-icon>mdi-pencil</v-icon>
</v-btn> </v-btn>
<!-- Action menu (bottom right) --> <!-- Action menu (bottom right) -->
<div v-if="!selected && !preselect && actionMenu" <div v-if="!selected && !preselect && actionMenu"
style="position: absolute; bottom: 5px; right: 5px" style="position: absolute; bottom: 5px; right: 5px"
> >
<book-actions-menu v-if="computedItem.type() === ItemTypes.BOOK" <book-actions-menu v-if="computedItem.type() === ItemTypes.BOOK"
:book="item" :book="item"
:menu.sync="actionMenuState" :menu.sync="actionMenuState"
/> />
<series-actions-menu v-if="computedItem.type() === ItemTypes.SERIES" <series-actions-menu v-if="computedItem.type() === ItemTypes.SERIES"
:series="item" :series="item"
:menu.sync="actionMenuState" :menu.sync="actionMenuState"
/> />
<collection-actions-menu v-if="computedItem.type() === ItemTypes.COLLECTION" <collection-actions-menu v-if="computedItem.type() === ItemTypes.COLLECTION"
:collection="item" :collection="item"
:menu.sync="actionMenuState" :menu.sync="actionMenuState"
/> />
</div> </div>
</v-overlay> </v-overlay>
</v-fade-transition> </v-fade-transition>
<v-progress-linear <v-progress-linear
v-if="isInProgress" v-if="isInProgress"
:value="readProgressPercentage" :value="readProgressPercentage"
color="orange" color="orange"
height="6" height="6"
style="position: absolute; bottom: 0" style="position: absolute; bottom: 0"
/> />
</v-img> </v-img>
<!-- Description--> <!-- Description-->
<template v-if="!thumbnailOnly"> <template v-if="!thumbnailOnly">
<router-link :to="to" class="link-underline"> <router-link :to="to" class="link-underline">
<v-card-subtitle <v-card-subtitle
v-line-clamp="2" v-line-clamp="2"
v-bind="subtitleProps" v-bind="subtitleProps"
v-html="title" v-html="title"
> >
</v-card-subtitle> </v-card-subtitle>
</router-link> </router-link>
<v-card-text class="px-2" v-html="body"> <v-card-text class="px-2" v-html="body">
</v-card-text> </v-card-text>
</template> </template>
</v-card> </v-card>
</template> </template>
</v-hover> </v-hover>
</template> </template>
<script lang="ts"> <script lang="ts">
import BookActionsMenu from '@/components/menus/BookActionsMenu.vue' import BookActionsMenu from '@/components/menus/BookActionsMenu.vue'
import CollectionActionsMenu from '@/components/menus/CollectionActionsMenu.vue' import CollectionActionsMenu from '@/components/menus/CollectionActionsMenu.vue'
import SeriesActionsMenu from '@/components/menus/SeriesActionsMenu.vue' import SeriesActionsMenu from '@/components/menus/SeriesActionsMenu.vue'
import { getReadProgress, getReadProgressPercentage } from '@/functions/book-progress' import { getReadProgress, getReadProgressPercentage } from '@/functions/book-progress'
import { ReadStatus } from '@/types/enum-books' import { ReadStatus } from '@/types/enum-books'
import { createItem, Item, ItemTypes } from '@/types/items' import { createItem, Item, ItemTypes } from '@/types/items'
import Vue from 'vue' import Vue from 'vue'
import { RawLocation } from 'vue-router' import { RawLocation } from 'vue-router'
export default Vue.extend({ export default Vue.extend({
name: 'ItemCard', name: 'ItemCard',
components: { BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu }, components: { BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu },
props: { props: {
item: { item: {
type: Object as () => BookDto | SeriesDto | CollectionDto, type: Object as () => BookDto | SeriesDto | CollectionDto,
required: true, required: true,
}, },
// hide the bottom part of the card // hide the bottom part of the card
thumbnailOnly: { thumbnailOnly: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// disables the default link on clicking the card // disables the default link on clicking the card
noLink: { noLink: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
width: { width: {
type: [String, Number], type: [String, Number],
required: false, required: false,
default: 150, default: 150,
}, },
// when true, card will show the active border and circle icon full // when true, card will show the active border and circle icon full
selected: { selected: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// when true, will display the border like if the card was hovered, and click anywhere will trigger onSelected // when true, will display the border like if the card was hovered, and click anywhere will trigger onSelected
preselect: { preselect: {
type: Boolean, type: Boolean,
required: false, required: false,
}, },
// callback function to call when selecting the card // callback function to call when selecting the card
onSelected: { onSelected: {
type: Function, type: Function,
default: undefined, default: undefined,
required: false, required: false,
}, },
// callback function for the edit button // callback function for the edit button
onEdit: { onEdit: {
type: Function, type: Function,
default: undefined, default: undefined,
required: false, required: false,
}, },
// action menu enabled or not // action menu enabled or not
actionMenu: { actionMenu: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, },
data: () => { data: () => {
return { return {
ItemTypes, ItemTypes,
actionMenuState: false, actionMenuState: false,
} }
}, },
computed: { computed: {
canReadPages (): boolean { canReadPages (): boolean {
return this.$store.getters.mePageStreaming && this.computedItem.type() === ItemTypes.BOOK return this.$store.getters.mePageStreaming && this.computedItem.type() === ItemTypes.BOOK
}, },
overlay (): boolean { overlay (): boolean {
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages || this.actionMenu return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages || this.actionMenu
}, },
computedItem (): Item<BookDto | SeriesDto | CollectionDto> { computedItem (): Item<BookDto | SeriesDto | CollectionDto> {
return createItem(this.item) return createItem(this.item)
}, },
disableHover (): boolean { disableHover (): boolean {
return !this.overlay return !this.overlay
}, },
thumbnailUrl (): string { thumbnailUrl (): string {
return this.computedItem.thumbnailUrl() return this.computedItem.thumbnailUrl()
}, },
title (): string { title (): string {
return this.computedItem.title() return this.computedItem.title()
}, },
subtitleProps (): Object { subtitleProps (): Object {
return this.computedItem.subtitleProps() return this.computedItem.subtitleProps()
}, },
body (): string { body (): string {
return this.computedItem.body() return this.computedItem.body()
}, },
isInProgress (): boolean { isInProgress (): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.IN_PROGRESS if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.IN_PROGRESS
return false return false
}, },
isUnread (): boolean { isUnread (): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.UNREAD if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.UNREAD
return false return false
}, },
unreadCount (): number | undefined { unreadCount (): number | undefined {
if (this.computedItem.type() === ItemTypes.SERIES) return (this.item as SeriesDto).booksUnreadCount if (this.computedItem.type() === ItemTypes.SERIES) return (this.item as SeriesDto).booksUnreadCount
return undefined return undefined
}, },
readProgressPercentage (): number { readProgressPercentage (): number {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgressPercentage(this.item as BookDto) if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgressPercentage(this.item as BookDto)
return 0 return 0
}, },
bookReady (): boolean { bookReady (): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) { if (this.computedItem.type() === ItemTypes.BOOK) {
return (this.item as BookDto).media.status === 'READY' return (this.item as BookDto).media.status === 'READY'
} }
return false return false
}, },
to (): RawLocation { to (): RawLocation {
return this.computedItem.to() return this.computedItem.to()
}, },
}, },
methods: { methods: {
onClick () { onClick () {
if (this.preselect && this.onSelected !== undefined) { if (this.preselect && this.onSelected !== undefined) {
this.selectItem() this.selectItem()
} else if (!this.noLink) { } else if (!this.noLink) {
this.goto() this.goto()
} }
}, },
goto () { goto () {
this.$router.push(this.computedItem.to()) this.$router.push(this.computedItem.to())
}, },
selectItem () { selectItem () {
if (this.onSelected !== undefined) { if (this.onSelected !== undefined) {
this.onSelected() this.onSelected()
} }
}, },
editItem () { editItem () {
if (this.onEdit !== undefined) { if (this.onEdit !== undefined) {
this.onEdit(this.item) this.onEdit(this.item)
} }
}, },
}, },
}) })
</script> </script>
<style> <style>
.no-link { .no-link {
cursor: default; cursor: default;
} }
.item-border { .item-border {
border: 3px solid var(--v-secondary-base); border: 3px solid var(--v-secondary-base);
} }
.item-border-transparent { .item-border-transparent {
border: 3px solid transparent; border: 3px solid transparent;
} }
.item-border-darken { .item-border-darken {
border: 3px solid var(--v-secondary-darken2); border: 3px solid var(--v-secondary-darken2);
} }
.overlay-full .v-overlay__content { .overlay-full .v-overlay__content {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.unread { .unread {
border-left: 25px solid transparent; border-left: 25px solid transparent;
border-right: 25px solid orange; border-right: 25px solid orange;
border-bottom: 25px solid transparent; border-bottom: 25px solid transparent;
height: 0; height: 0;
width: 0; width: 0;
position: absolute; position: absolute;
right: 0; right: 0;
z-index: 2; z-index: 2;
} }
.link-underline { .link-underline {
text-decoration: none; text-decoration: none;
} }
.link-underline:hover { .link-underline:hover {
text-decoration: underline; text-decoration: underline;
text-decoration-color: black; text-decoration-color: black;
} }
</style> </style>