BookInfoDialog.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <template>
  2. <Dialog ref="dialog" v-model="dialogVisible">
  3. <template #header>
  4. <div class="row items-center">
  5. <div style="font-size: 110%">
  6. Информация о книге
  7. </div>
  8. </div>
  9. </template>
  10. <div ref="box" class="fit column q-mt-xs overflow-auto no-wrap" style="padding: 0px 10px 10px 10px;">
  11. <div class="text-green-10">
  12. {{ bookAuthor }}
  13. </div>
  14. <div>
  15. <b>{{ book.title }}</b>
  16. </div>
  17. <div class="row q-mt-sm no-wrap">
  18. <div class="poster-size">
  19. <div class="column justify-center items-center" :class="{'poster': coverSrc, 'no-poster': !coverSrc}" @click.stop.prevent="posterClick">
  20. <img v-if="coverSrc" :src="coverSrc" class="fit row justify-center items-center" style="object-fit: contain" @error="coverSrc = ''" />
  21. <div v-if="!coverSrc" class="fit row justify-center items-center text-grey-5 overflow-hidden" style="border: 1px solid #ccc; font-size: 300%">
  22. <i>{{ book.ext }}</i>
  23. </div>
  24. </div>
  25. </div>
  26. <div class="col column q-ml-sm" style="min-width: 400px; border: 1px solid #ccc">
  27. <div class="bg-grey-3 row">
  28. <q-tabs
  29. v-model="selectedTab"
  30. active-color="black"
  31. active-bg-color="white"
  32. indicator-color="white"
  33. dense
  34. no-caps
  35. inline-label
  36. class="bg-grey-4 text-grey-7"
  37. >
  38. <q-tab v-if="fb2.length" name="fb2" label="Fb2 инфо" />
  39. <q-tab name="inpx" label="Inpx инфо" />
  40. </q-tabs>
  41. </div>
  42. <div class="overflow-auto full-width" style="height: 262px">
  43. <div v-for="item in info" :key="item.name">
  44. <div class="row q-ml-sm q-mt-sm items-center">
  45. <div class="text-blue" style="font-size: 90%">
  46. {{ item.label }}
  47. </div>
  48. <div class="col q-mx-xs" style="height: 0px; border-top: 1px solid #ccc"></div>
  49. </div>
  50. <div v-for="subItem in item.value" :key="subItem.name" class="row q-ml-md">
  51. <div style="width: 100px">
  52. {{ subItem.label }}
  53. </div>
  54. <div class="q-ml-sm" v-html="subItem.value" />
  55. </div>
  56. </div>
  57. <div class="q-mt-xs"></div>
  58. </div>
  59. </div>
  60. </div>
  61. <div class="q-mt-md" v-html="annotation" />
  62. </div>
  63. <template #footer>
  64. <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
  65. OK
  66. </q-btn>
  67. </template>
  68. <Dialog v-model="posterDialogVisible">
  69. <template #header>
  70. <div class="row items-center">
  71. <div style="font-size: 110%">
  72. Обложка
  73. </div>
  74. </div>
  75. </template>
  76. <img :src="coverSrc" class="fit q-pb-sm" style="height: 100%; max-height: calc(100vh - 140px); object-fit: contain" />
  77. </Dialog>
  78. </Dialog>
  79. </template>
  80. <script>
  81. //-----------------------------------------------------------------------------
  82. import vueComponent from '../../vueComponent.js';
  83. import Dialog from '../../share/Dialog.vue';
  84. import Fb2Parser from '../../../../server/core/fb2/Fb2Parser';
  85. import * as utils from '../../../share/utils';
  86. import _ from 'lodash';
  87. const componentOptions = {
  88. components: {
  89. Dialog
  90. },
  91. watch: {
  92. modelValue(newValue) {
  93. this.dialogVisible = newValue;
  94. if (newValue)
  95. this.init();
  96. },
  97. dialogVisible(newValue) {
  98. this.$emit('update:modelValue', newValue);
  99. },
  100. }
  101. };
  102. class BookInfoDialog {
  103. _options = componentOptions;
  104. _props = {
  105. modelValue: Boolean,
  106. bookInfo: Object,
  107. genreMap: Object,
  108. };
  109. dialogVisible = false;
  110. posterDialogVisible = false;
  111. selectedTab = 'fb2';
  112. //info props
  113. coverSrc = '';
  114. annotation = '';
  115. fb2 = [];
  116. book = {};
  117. created() {
  118. this.commit = this.$store.commit;
  119. }
  120. mounted() {
  121. }
  122. init() {
  123. //defaults
  124. this.coverSrc = '';
  125. this.annotation = '';
  126. this.fb2 = [];
  127. this.book = {};
  128. this.parseBookInfo();
  129. if (!this.fb2.length)
  130. this.selectedTab = 'inpx';
  131. }
  132. get bookAuthor() {
  133. if (this.book.author) {
  134. let a = this.book.author.split(',');
  135. return a.slice(0, 3).join(', ') + (a.length > 3 ? ' и др.' : '');
  136. }
  137. return '';
  138. }
  139. formatSize(size) {
  140. size = size/1024;
  141. let unit = 'KB';
  142. if (size > 1024) {
  143. size = size/1024;
  144. unit = 'MB';
  145. }
  146. return `${size.toFixed(1)} ${unit}`;
  147. }
  148. convertGenres(genreArr) {
  149. let result = [];
  150. if (genreArr) {
  151. for (const genre of genreArr) {
  152. const g = genre.trim();
  153. const name = this.genreMap.get(g);
  154. result.push(name ? name : g);
  155. }
  156. }
  157. return result.join(', ');
  158. }
  159. get inpx() {
  160. const mapping = [
  161. {name: 'fileInfo', label: 'Информация о файле', value: [
  162. {name: 'folder', label: 'Папка'},
  163. {name: 'file', label: 'Файл'},
  164. {name: 'size', label: 'Размер'},
  165. {name: 'date', label: 'Добавлен'},
  166. {name: 'del', label: 'Удален'},
  167. {name: 'libid', label: 'LibId'},
  168. {name: 'insno', label: 'InsideNo'},
  169. ]},
  170. {name: 'titleInfo', label: 'Общая информация', value: [
  171. {name: 'author', label: 'Автор(ы)'},
  172. {name: 'title', label: 'Название'},
  173. {name: 'series', label: 'Серия'},
  174. {name: 'genre', label: 'Жанр'},
  175. {name: 'librate', label: 'Оценка'},
  176. {name: 'lang', label: 'Язык книги'},
  177. {name: 'keywords', label: 'Ключевые слова'},
  178. ]},
  179. ];
  180. const valueToString = (value, nodePath, b) => {//eslint-disable-line no-unused-vars
  181. if (nodePath == 'fileInfo/file')
  182. return `${value}.${b.ext}`;
  183. if (nodePath == 'fileInfo/size')
  184. return `${this.formatSize(value)} (${value.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ')} Bytes)`;
  185. if (nodePath == 'fileInfo/date')
  186. return utils.sqlDateFormat(value);
  187. if (nodePath == 'fileInfo/del')
  188. return (value ? 'Да' : null);
  189. if (nodePath == 'fileInfo/insno')
  190. return (value ? value : null);
  191. if (nodePath == 'titleInfo/author')
  192. return value.split(',').join(', ');
  193. if (nodePath == 'titleInfo/genre')
  194. return this.convertGenres(value.split(','));
  195. if (nodePath == 'titleInfo/librate' && !value)
  196. return null;
  197. if (typeof(value) === 'string') {
  198. return value;
  199. }
  200. return (value.toString ? value.toString() : '');
  201. };
  202. let result = [];
  203. const book = _.cloneDeep(this.book);
  204. book.series = [book.series, book.serno].filter(v => v).join(' #');
  205. for (const item of mapping) {
  206. const itemOut = {name: item.name, label: item.label, value: []};
  207. for (const subItem of item.value) {
  208. const subItemOut = {
  209. name: subItem.name,
  210. label: subItem.label,
  211. value: valueToString(book[subItem.name], `${item.name}/${subItem.name}`, book)
  212. };
  213. if (subItemOut.value)
  214. itemOut.value.push(subItemOut);
  215. }
  216. if (itemOut.value.length)
  217. result.push(itemOut);
  218. }
  219. return result;
  220. }
  221. get info() {
  222. let result = [];
  223. switch (this.selectedTab) {
  224. case 'fb2':
  225. return this.fb2;
  226. case 'inpx':
  227. return this.inpx;
  228. }
  229. return result;
  230. }
  231. parseBookInfo() {
  232. const bookInfo = this.bookInfo;
  233. //cover
  234. if (bookInfo.cover)
  235. this.coverSrc = bookInfo.cover;
  236. //fb2
  237. if (bookInfo.fb2) {
  238. const parser = new Fb2Parser(bookInfo.fb2);
  239. const infoObj = parser.bookInfo();
  240. if (infoObj.titleInfo) {
  241. let ann = infoObj.titleInfo.annotationHtml;
  242. if (ann) {
  243. ann = ann.replace(/<p>/g, `<p class="p-annotation">`);
  244. this.annotation = ann;
  245. }
  246. }
  247. const self = this;
  248. this.fb2 = parser.bookInfoList(infoObj, {
  249. valueToString(value, nodePath, origVTS) {//eslint-disable-line no-unused-vars
  250. if (nodePath == 'documentInfo/historyHtml' && value)
  251. return value.replace(/<p>/g, `<p class="p-history">`);
  252. if ((nodePath == 'titleInfo/genre' || nodePath == 'srcTitleInfo/genre') && value) {
  253. return self.convertGenres(value);
  254. }
  255. return origVTS(value, nodePath);
  256. },
  257. });
  258. }
  259. //book
  260. if (bookInfo.book)
  261. this.book = bookInfo.book;
  262. }
  263. posterClick() {
  264. if (!this.coverSrc)
  265. return;
  266. this.posterDialogVisible = true;
  267. }
  268. okClick() {
  269. this.dialogVisible = false;
  270. }
  271. }
  272. export default vueComponent(BookInfoDialog);
  273. //-----------------------------------------------------------------------------
  274. </script>
  275. <style scoped>
  276. .poster-size {
  277. height: 300px;
  278. width: 200px;
  279. min-width: 100px;
  280. }
  281. .poster, .no-poster {
  282. width: 100%;
  283. height: 100%;
  284. }
  285. .poster:hover {
  286. position: relative;
  287. top: -1%;
  288. left: -1%;
  289. width: 102%;
  290. height: 102%;
  291. cursor: pointer;
  292. }
  293. </style>
  294. <style>
  295. .p-annotation {
  296. text-indent: 20px;
  297. text-align: justify;
  298. padding: 0;
  299. margin: 0;
  300. }
  301. .p-history {
  302. padding: 0;
  303. margin: 0;
  304. }
  305. </style>