mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Fix: root user password change #93, Change: link to series filter on ab page #90, Add: basic mobi and azw3 ereader support
This commit is contained in:
parent
120c70622a
commit
9715c53332
405
client/assets/mobi.js
Normal file
405
client/assets/mobi.js
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
function ab2str(buf) {
|
||||||
|
if (buf instanceof ArrayBuffer) {
|
||||||
|
buf = new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
return new TextDecoder("utf-8").decode(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
var domParser = new DOMParser();
|
||||||
|
|
||||||
|
class Buffer {
|
||||||
|
constructor(capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.fragment_list = [];
|
||||||
|
this.cur_fragment = new Fragment(capacity);
|
||||||
|
this.fragment_list.push(this.cur_fragment);
|
||||||
|
}
|
||||||
|
write(byte) {
|
||||||
|
var result = this.cur_fragment.write(byte);
|
||||||
|
if (!result) {
|
||||||
|
this.cur_fragment = new Fragment(this.capacity);
|
||||||
|
this.fragment_list.push(this.cur_fragment);
|
||||||
|
this.cur_fragment.write(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get(idx) {
|
||||||
|
var fi = 0;
|
||||||
|
while (fi < this.fragment_list.length) {
|
||||||
|
var frag = this.fragment_list[fi];
|
||||||
|
if (idx < frag.size) {
|
||||||
|
return frag.get(idx);
|
||||||
|
}
|
||||||
|
idx -= frag.size;
|
||||||
|
fi += 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
size() {
|
||||||
|
var s = 0;
|
||||||
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
||||||
|
s += this.fragment_list[i].size;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
shrink() {
|
||||||
|
var total_buffer = new Uint8Array(this.size());
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
||||||
|
var frag = this.fragment_list[i];
|
||||||
|
if (frag.full()) {
|
||||||
|
total_buffer.set(frag.buffer, offset);
|
||||||
|
} else {
|
||||||
|
total_buffer.set(frag.buffer.slice(0, frag.size), offset);
|
||||||
|
}
|
||||||
|
offset += frag.size;
|
||||||
|
}
|
||||||
|
return total_buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var combine_uint8array = function (buffers) {
|
||||||
|
var total_size = 0;
|
||||||
|
for (var i = 0; i < buffers.length; i++) {
|
||||||
|
var buffer = buffers[i];
|
||||||
|
total_size += buffer.length;
|
||||||
|
}
|
||||||
|
var total_buffer = new Uint8Array(total_size);
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < buffers.length; i++) {
|
||||||
|
var buffer = buffers[i];
|
||||||
|
total_buffer.set(buffer, offset);
|
||||||
|
offset += buffer.length;
|
||||||
|
}
|
||||||
|
return total_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fragment {
|
||||||
|
constructor(capacity) {
|
||||||
|
this.buffer = new Uint8Array(capacity);
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(byte) {
|
||||||
|
if (this.size >= this.capacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.buffer[this.size] = byte;
|
||||||
|
this.size += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
full() {
|
||||||
|
return this.size === this.capacity;
|
||||||
|
}
|
||||||
|
get(idx) {
|
||||||
|
return this.buffer[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uncompression_lz77 = function (data) {
|
||||||
|
var length = data.length;
|
||||||
|
var offset = 0; // Current offset into data
|
||||||
|
var buffer = new Buffer(data.length);
|
||||||
|
|
||||||
|
while (offset < length) {
|
||||||
|
var char = data[offset];
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
if (char == 0) {
|
||||||
|
buffer.write(char);
|
||||||
|
} else if (char <= 8) {
|
||||||
|
for (var i = offset; i < offset + char; i++) {
|
||||||
|
buffer.write(data[i]);
|
||||||
|
}
|
||||||
|
offset += char;
|
||||||
|
} else if (char <= 0x7f) {
|
||||||
|
buffer.write(char);
|
||||||
|
} else if (char <= 0xbf) {
|
||||||
|
var next = data[offset];
|
||||||
|
offset += 1;
|
||||||
|
var distance = ((char << 8 | next) >> 3) & 0x7ff;
|
||||||
|
var lz_length = (next & 0x7) + 3;
|
||||||
|
|
||||||
|
var buffer_size = buffer.size();
|
||||||
|
for (var i = 0; i < lz_length; i++) {
|
||||||
|
buffer.write(buffer.get(buffer_size - distance))
|
||||||
|
buffer_size += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.write(32);
|
||||||
|
buffer.write(char ^ 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MobiFile {
|
||||||
|
constructor(data) {
|
||||||
|
this.view = new DataView(data);
|
||||||
|
this.buffer = this.view.buffer;
|
||||||
|
this.offset = 0;
|
||||||
|
this.header = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint8() {
|
||||||
|
var v = this.view.getUint8(this.offset);
|
||||||
|
this.offset += 1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint16() {
|
||||||
|
var v = this.view.getUint16(this.offset);
|
||||||
|
this.offset += 2;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint32() {
|
||||||
|
var v = this.view.getUint32(this.offset);
|
||||||
|
this.offset += 4;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStr(size) {
|
||||||
|
var v = ab2str(this.buffer.slice(this.offset, this.offset + size));
|
||||||
|
this.offset += size;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(size) {
|
||||||
|
this.offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
setoffset(_of) {
|
||||||
|
this.offset = _of;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_record_extrasize(data, flags) {
|
||||||
|
var pos = data.length - 1;
|
||||||
|
var extra = 0;
|
||||||
|
for (var i = 15; i > 0; i--) {
|
||||||
|
if (flags & (1 << i)) {
|
||||||
|
var res = this.buffer_get_varlen(data, pos);
|
||||||
|
var size = res[0];
|
||||||
|
var l = res[1];
|
||||||
|
pos = res[2];
|
||||||
|
pos -= size - l;
|
||||||
|
extra += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags & 1) {
|
||||||
|
var a = data[pos];
|
||||||
|
extra += (a & 0x3) + 1;
|
||||||
|
}
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
// data should be uint8array
|
||||||
|
buffer_get_varlen(data, pos) {
|
||||||
|
var l = 0;
|
||||||
|
var size = 0;
|
||||||
|
var byte_count = 0;
|
||||||
|
var mask = 0x7f;
|
||||||
|
var stop_flag = 0x80;
|
||||||
|
var shift = 0;
|
||||||
|
for (var i = 0; ; i++) {
|
||||||
|
var byte = data[pos];
|
||||||
|
size |= (byte & mask) << shift;
|
||||||
|
shift += 7;
|
||||||
|
l += 1;
|
||||||
|
byte_count += 1;
|
||||||
|
pos -= 1;
|
||||||
|
|
||||||
|
var to_stop = byte & stop_flag;
|
||||||
|
if (byte_count >= 4 || to_stop > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [size, l, pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
read_text() {
|
||||||
|
var text_end = this.palm_header.record_count;
|
||||||
|
var buffers = [];
|
||||||
|
for (var i = 1; i <= text_end; i++) {
|
||||||
|
buffers.push(this.read_text_record(i));
|
||||||
|
}
|
||||||
|
var all = combine_uint8array(buffers)
|
||||||
|
return ab2str(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
read_text_record(i) {
|
||||||
|
var flags = this.mobi_header.extra_flags;
|
||||||
|
var begin = this.reclist[i].offset;
|
||||||
|
var end = this.reclist[i + 1].offset;
|
||||||
|
|
||||||
|
var data = new Uint8Array(this.buffer.slice(begin, end))
|
||||||
|
var ex = this.get_record_extrasize(data, flags);
|
||||||
|
|
||||||
|
data = new Uint8Array(this.buffer.slice(begin, end - ex));
|
||||||
|
if (this.palm_header.compression === 2) {
|
||||||
|
var buffer = uncompression_lz77(data);
|
||||||
|
return buffer.shrink();
|
||||||
|
} else {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read_image(idx) {
|
||||||
|
var first_image_idx = this.mobi_header.first_image_idx;
|
||||||
|
var begin = this.reclist[first_image_idx + idx].offset;
|
||||||
|
var end = this.reclist[first_image_idx + idx + 1].offset;
|
||||||
|
var data = new Uint8Array(this.buffer.slice(begin, end))
|
||||||
|
return new Blob([data.buffer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.header = this.load_pdbheader();
|
||||||
|
this.reclist = this.load_reclist();
|
||||||
|
this.load_record0();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_pdbheader() {
|
||||||
|
var header = {};
|
||||||
|
header.name = this.getStr(32);
|
||||||
|
header.attr = this.getUint16();
|
||||||
|
header.version = this.getUint16();
|
||||||
|
header.ctime = this.getUint32();
|
||||||
|
header.mtime = this.getUint32();
|
||||||
|
header.btime = this.getUint32();
|
||||||
|
header.mod_num = this.getUint32();
|
||||||
|
header.appinfo_offset = this.getUint32();
|
||||||
|
header.sortinfo_offset = this.getUint32();
|
||||||
|
header.type = this.getStr(4);
|
||||||
|
header.creator = this.getStr(4);
|
||||||
|
header.uid = this.getUint32();
|
||||||
|
header.next_rec = this.getUint32();
|
||||||
|
header.record_num = this.getUint16();
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_reclist() {
|
||||||
|
var reclist = [];
|
||||||
|
for (var i = 0; i < this.header.record_num; i++) {
|
||||||
|
var record = {};
|
||||||
|
record.offset = this.getUint32();
|
||||||
|
// TODO(zz) change
|
||||||
|
record.attr = this.getUint32();
|
||||||
|
reclist.push(record);
|
||||||
|
}
|
||||||
|
return reclist;
|
||||||
|
}
|
||||||
|
load_record0() {
|
||||||
|
this.palm_header = this.load_record0_header();
|
||||||
|
this.mobi_header = this.load_mobi_header();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_record0_header() {
|
||||||
|
var p_header = {};
|
||||||
|
var first_record = this.reclist[0];
|
||||||
|
this.setoffset(first_record.offset);
|
||||||
|
|
||||||
|
p_header.compression = this.getUint16();
|
||||||
|
this.skip(2);
|
||||||
|
p_header.text_length = this.getUint32();
|
||||||
|
p_header.record_count = this.getUint16();
|
||||||
|
p_header.record_size = this.getUint16();
|
||||||
|
p_header.encryption_type = this.getUint16();
|
||||||
|
this.skip(2);
|
||||||
|
|
||||||
|
return p_header;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_mobi_header() {
|
||||||
|
var mobi_header = {};
|
||||||
|
|
||||||
|
var start_offset = this.offset;
|
||||||
|
|
||||||
|
mobi_header.identifier = this.getUint32();
|
||||||
|
mobi_header.header_length = this.getUint32();
|
||||||
|
mobi_header.mobi_type = this.getUint32();
|
||||||
|
mobi_header.text_encoding = this.getUint32();
|
||||||
|
mobi_header.uid = this.getUint32();
|
||||||
|
mobi_header.generator_version = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(40);
|
||||||
|
|
||||||
|
mobi_header.first_nonbook_index = this.getUint32();
|
||||||
|
mobi_header.full_name_offset = this.getUint32();
|
||||||
|
mobi_header.full_name_length = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.language = this.getUint32();
|
||||||
|
mobi_header.input_language = this.getUint32();
|
||||||
|
mobi_header.output_language = this.getUint32();
|
||||||
|
mobi_header.min_version = this.getUint32();
|
||||||
|
mobi_header.first_image_idx = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.huff_rec_index = this.getUint32();
|
||||||
|
mobi_header.huff_rec_count = this.getUint32();
|
||||||
|
mobi_header.datp_rec_index = this.getUint32();
|
||||||
|
mobi_header.datp_rec_count = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.exth_flags = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(36);
|
||||||
|
|
||||||
|
mobi_header.drm_offset = this.getUint32();
|
||||||
|
mobi_header.drm_count = this.getUint32();
|
||||||
|
mobi_header.drm_size = this.getUint32();
|
||||||
|
mobi_header.drm_flags = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(8);
|
||||||
|
|
||||||
|
// TODO (zz) fdst_index
|
||||||
|
this.skip(4);
|
||||||
|
|
||||||
|
this.skip(46);
|
||||||
|
|
||||||
|
mobi_header.extra_flags = this.getUint16();
|
||||||
|
|
||||||
|
this.setoffset(start_offset + mobi_header.header_length)
|
||||||
|
|
||||||
|
return mobi_header;
|
||||||
|
}
|
||||||
|
load_exth_header() {
|
||||||
|
// TODO
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render_to(id) {
|
||||||
|
this.load();
|
||||||
|
var content = this.read_text();
|
||||||
|
|
||||||
|
var bookDom = document.getElementById(id);
|
||||||
|
while (bookDom.firstChild) {
|
||||||
|
bookDom.removeChild(bookDom.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bookDoc = domParser.parseFromString(content, "text/html");
|
||||||
|
bookDoc.body.childNodes.forEach(function (x) {
|
||||||
|
if (x instanceof Element) {
|
||||||
|
bookDom.appendChild(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var imgDoms = bookDom.getElementsByTagName("img");
|
||||||
|
for (var i = 0; i < imgDoms.length; i++) {
|
||||||
|
this.render_image(imgDoms, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render_image(imgDoms, i) {
|
||||||
|
var imgDom = imgDoms[i];
|
||||||
|
var idx = +imgDom.getAttribute("recindex");
|
||||||
|
var blob = this.read_image(idx - 1);
|
||||||
|
var imgReader = new FileReader();
|
||||||
|
imgReader.onload = function (e) {
|
||||||
|
imgDom.src = e.target.result;
|
||||||
|
};
|
||||||
|
imgReader.readAsDataURL(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = MobiFile
|
@ -9,11 +9,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="absolute top-4 left-4 font-book">
|
<div class="absolute top-4 left-4 font-book">
|
||||||
<h1 class="text-2xl mb-1">{{ title }}</h1>
|
<h1 class="text-2xl mb-1">{{ title || abTitle }}</h1>
|
||||||
|
<p v-if="author || abAuthor">by {{ author || abAuthor }}</p>
|
||||||
<p v-if="author">by {{ author }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex items-center">
|
<div v-if="!epubEbook && mobiEbook" class="absolute top-4 left-0 w-full flex justify-center">
|
||||||
|
<p class="text-error font-semibold">Warning: Reading mobi & azw3 files is in the very early stages</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="epubEbook" class="h-full flex items-center">
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
|
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
|
||||||
<span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span>
|
<span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span>
|
||||||
</div>
|
</div>
|
||||||
@ -28,11 +31,17 @@
|
|||||||
<span v-show="hasNext" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageRight">chevron_right</span>
|
<span v-show="hasNext" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageRight">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="h-full flex items-center justify-center">
|
||||||
|
<div class="w-full max-w-4xl overflow-y-auto border border-black border-opacity-10 p-4" style="max-height: 80vh">
|
||||||
|
<div id="viewer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ePub from 'epubjs'
|
import ePub from 'epubjs'
|
||||||
|
import mobijs from '@/assets/mobi.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@ -65,6 +74,12 @@ export default {
|
|||||||
this.$store.commit('setShowEReader', val)
|
this.$store.commit('setShowEReader', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
abTitle() {
|
||||||
|
return this.selectedAudiobook.book.title
|
||||||
|
},
|
||||||
|
abAuthor() {
|
||||||
|
return this.selectedAudiobook.book.author
|
||||||
|
},
|
||||||
selectedAudiobook() {
|
selectedAudiobook() {
|
||||||
return this.$store.state.selectedAudiobook
|
return this.$store.state.selectedAudiobook
|
||||||
},
|
},
|
||||||
@ -83,6 +98,16 @@ export default {
|
|||||||
epubPath() {
|
epubPath() {
|
||||||
return this.epubEbook ? this.epubEbook.path : null
|
return this.epubEbook ? this.epubEbook.path : null
|
||||||
},
|
},
|
||||||
|
mobiEbook() {
|
||||||
|
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
|
||||||
|
},
|
||||||
|
mobiPath() {
|
||||||
|
return this.mobiEbook ? this.mobiEbook.path : null
|
||||||
|
},
|
||||||
|
mobiUrl() {
|
||||||
|
if (!this.mobiPath) return null
|
||||||
|
return `/ebook/${this.libraryId}/${this.folderId}/${this.mobiPath}`
|
||||||
|
},
|
||||||
url() {
|
url() {
|
||||||
if (!this.epubPath) return null
|
if (!this.epubPath) return null
|
||||||
return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
|
return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
|
||||||
@ -134,7 +159,24 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
this.registerListeners()
|
this.registerListeners()
|
||||||
|
|
||||||
console.log('epub', this.url, this.epubEbook, this.ebooks)
|
if (this.epubEbook) {
|
||||||
|
this.initEpub()
|
||||||
|
} else if (this.mobiEbook) {
|
||||||
|
this.initMobi()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async initMobi() {
|
||||||
|
var buff = await this.$axios.$get(this.mobiUrl, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = function (event) {
|
||||||
|
var file_content = event.target.result
|
||||||
|
new mobijs(file_content).render_to('viewer')
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(buff)
|
||||||
|
},
|
||||||
|
initEpub() {
|
||||||
// var book = ePub(this.url, {
|
// var book = ePub(this.url, {
|
||||||
// requestHeaders: {
|
// requestHeaders: {
|
||||||
// Authorization: `Bearer ${this.userToken}`
|
// Authorization: `Bearer ${this.userToken}`
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.5",
|
"version": "1.4.6",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<p v-if="isRoot" class="text-error py-2 text-xs">* Root user is the only user that can have an empty password</p>
|
<p v-if="isRoot" class="text-error py-2 text-xs">* Root user is the only user that can have an empty password</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-show="password && newPassword && confirmPassword" type="submit" :loading="changingPassword" color="success">Submit</ui-btn>
|
<ui-btn v-show="(password && newPassword && confirmPassword) || isRoot" type="submit" :loading="changingPassword" color="success">Submit</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,8 +22,7 @@
|
|||||||
by <nuxt-link v-if="author" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link
|
by <nuxt-link v-if="author" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link
|
||||||
><span v-else>Unknown</span>
|
><span v-else>Unknown</span>
|
||||||
</p>
|
</p>
|
||||||
|
<nuxt-link v-if="series" :to="`/library/${libraryId}/bookshelf?filter=series.${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7 mb-4"> {{ seriesText }}</nuxt-link>
|
||||||
<h3 v-if="series" class="font-sans text-gray-300 text-lg leading-7 mb-4">{{ seriesText }}</h3>
|
|
||||||
|
|
||||||
<!-- <div class="w-min">
|
<!-- <div class="w-min">
|
||||||
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
||||||
@ -105,7 +104,7 @@
|
|||||||
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-if="showExperimentalFeatures && epubEbook" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
<ui-btn v-if="showExperimentalFeatures && (epubEbook || mobiEbook)" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||||
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
||||||
Read
|
Read
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@ -329,7 +328,10 @@ export default {
|
|||||||
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
epubEbook() {
|
epubEbook() {
|
||||||
return this.audiobook.ebooks.find((eb) => eb.ext === '.epub')
|
return this.ebooks.find((eb) => eb.ext === '.epub')
|
||||||
|
},
|
||||||
|
mobiEbook() {
|
||||||
|
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
@ -455,6 +457,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||||
|
|
||||||
|
// If a library has not yet been loaded, use this audiobooks library id as the current
|
||||||
|
if (!this.$store.state.audiobooks.loadedLibraryId && this.libraryId) {
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||||
|
@ -113,7 +113,6 @@ export const mutations = {
|
|||||||
state.showEditModal = val
|
state.showEditModal = val
|
||||||
},
|
},
|
||||||
showEReader(state, audiobook) {
|
showEReader(state, audiobook) {
|
||||||
console.log('Show EReader', audiobook)
|
|
||||||
state.selectedAudiobook = audiobook
|
state.selectedAudiobook = audiobook
|
||||||
state.showEReader = true
|
state.showEReader = true
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.4.5",
|
"version": "1.4.6",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -131,6 +131,10 @@ class Audiobook {
|
|||||||
return this.otherFiles.find(file => file.ext === '.epub')
|
return this.otherFiles.find(file => file.ext === '.epub')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasMobi() {
|
||||||
|
return this.otherFiles.find(file => file.ext === '.mobi' || file.ext === '.azw3')
|
||||||
|
}
|
||||||
|
|
||||||
get hasMissingIno() {
|
get hasMissingIno() {
|
||||||
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
|
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
|
||||||
}
|
}
|
||||||
@ -202,7 +206,7 @@ class Audiobook {
|
|||||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
||||||
// numEbooks: this.ebooks.length,
|
// numEbooks: this.ebooks.length,
|
||||||
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
|
||||||
numEbooks: this.hasEpub ? 1 : 0, // Only supporting epubs in the reader currently
|
numEbooks: (this.hasEpub || this.hasMobi) ? 1 : 0, // Only supporting epubs in the reader currently
|
||||||
numTracks: this.tracks.length,
|
numTracks: this.tracks.length,
|
||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const globals = {
|
const globals = {
|
||||||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
|
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
|
||||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi']
|
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = globals
|
module.exports = globals
|
||||||
|
404
server/utils/mobi.js
Normal file
404
server/utils/mobi.js
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
function ab2str(buf) {
|
||||||
|
if (buf instanceof ArrayBuffer) {
|
||||||
|
buf = new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
return new TextDecoder("utf-8").decode(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
var domParser = new DOMParser();
|
||||||
|
|
||||||
|
class Buffer {
|
||||||
|
constructor(capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.fragment_list = [];
|
||||||
|
this.cur_fragment = new Fragment(capacity);
|
||||||
|
this.fragment_list.push(this.cur_fragment);
|
||||||
|
}
|
||||||
|
write(byte) {
|
||||||
|
var result = this.cur_fragment.write(byte);
|
||||||
|
if (!result) {
|
||||||
|
this.cur_fragment = new Fragment(this.capacity);
|
||||||
|
this.fragment_list.push(this.cur_fragment);
|
||||||
|
this.cur_fragment.write(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get(idx) {
|
||||||
|
var fi = 0;
|
||||||
|
while (fi < this.fragment_list.length) {
|
||||||
|
var frag = this.fragment_list[fi];
|
||||||
|
if (idx < frag.size) {
|
||||||
|
return frag.get(idx);
|
||||||
|
}
|
||||||
|
idx -= frag.size;
|
||||||
|
fi += 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
size() {
|
||||||
|
var s = 0;
|
||||||
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
||||||
|
s += this.fragment_list[i].size;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
shrink() {
|
||||||
|
var total_buffer = new Uint8Array(this.size());
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
||||||
|
var frag = this.fragment_list[i];
|
||||||
|
if (frag.full()) {
|
||||||
|
total_buffer.set(frag.buffer, offset);
|
||||||
|
} else {
|
||||||
|
total_buffer.set(frag.buffer.slice(0, frag.size), offset);
|
||||||
|
}
|
||||||
|
offset += frag.size;
|
||||||
|
}
|
||||||
|
return total_buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var combine_uint8array = function (buffers) {
|
||||||
|
var total_size = 0;
|
||||||
|
for (var i = 0; i < buffers.length; i++) {
|
||||||
|
var buffer = buffers[i];
|
||||||
|
total_size += buffer.length;
|
||||||
|
}
|
||||||
|
var total_buffer = new Uint8Array(total_size);
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < buffers.length; i++) {
|
||||||
|
var buffer = buffers[i];
|
||||||
|
total_buffer.set(buffer, offset);
|
||||||
|
offset += buffer.length;
|
||||||
|
}
|
||||||
|
return total_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fragment {
|
||||||
|
constructor(capacity) {
|
||||||
|
this.buffer = new Uint8Array(capacity);
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(byte) {
|
||||||
|
if (this.size >= this.capacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.buffer[this.size] = byte;
|
||||||
|
this.size += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
full() {
|
||||||
|
return this.size === this.capacity;
|
||||||
|
}
|
||||||
|
get(idx) {
|
||||||
|
return this.buffer[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uncompression_lz77 = function (data) {
|
||||||
|
var length = data.length;
|
||||||
|
var offset = 0; // Current offset into data
|
||||||
|
var buffer = new Buffer(data.length);
|
||||||
|
|
||||||
|
while (offset < length) {
|
||||||
|
var char = data[offset];
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
if (char == 0) {
|
||||||
|
buffer.write(char);
|
||||||
|
} else if (char <= 8) {
|
||||||
|
for (var i = offset; i < offset + char; i++) {
|
||||||
|
buffer.write(data[i]);
|
||||||
|
}
|
||||||
|
offset += char;
|
||||||
|
} else if (char <= 0x7f) {
|
||||||
|
buffer.write(char);
|
||||||
|
} else if (char <= 0xbf) {
|
||||||
|
var next = data[offset];
|
||||||
|
offset += 1;
|
||||||
|
var distance = ((char << 8 | next) >> 3) & 0x7ff;
|
||||||
|
var lz_length = (next & 0x7) + 3;
|
||||||
|
|
||||||
|
var buffer_size = buffer.size();
|
||||||
|
for (var i = 0; i < lz_length; i++) {
|
||||||
|
buffer.write(buffer.get(buffer_size - distance))
|
||||||
|
buffer_size += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.write(32);
|
||||||
|
buffer.write(char ^ 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MobiFile {
|
||||||
|
constructor(data) {
|
||||||
|
this.view = new DataView(data);
|
||||||
|
this.buffer = this.view.buffer;
|
||||||
|
this.offset = 0;
|
||||||
|
this.header = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint8() {
|
||||||
|
var v = this.view.getUint8(this.offset);
|
||||||
|
this.offset += 1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint16() {
|
||||||
|
var v = this.view.getUint16(this.offset);
|
||||||
|
this.offset += 2;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUint32() {
|
||||||
|
var v = this.view.getUint32(this.offset);
|
||||||
|
this.offset += 4;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStr(size) {
|
||||||
|
var v = ab2str(this.buffer.slice(this.offset, this.offset + size));
|
||||||
|
this.offset += size;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(size) {
|
||||||
|
this.offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
setoffset(_of) {
|
||||||
|
this.offset = _of;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_record_extrasize(data, flags) {
|
||||||
|
var pos = data.length - 1;
|
||||||
|
var extra = 0;
|
||||||
|
for (var i = 15; i > 0; i--) {
|
||||||
|
if (flags & (1 << i)) {
|
||||||
|
var res = this.buffer_get_varlen(data, pos);
|
||||||
|
var size = res[0];
|
||||||
|
var l = res[1];
|
||||||
|
pos = res[2];
|
||||||
|
pos -= size - l;
|
||||||
|
extra += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags & 1) {
|
||||||
|
var a = data[pos];
|
||||||
|
extra += (a & 0x3) + 1;
|
||||||
|
}
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
// data should be uint8array
|
||||||
|
buffer_get_varlen(data, pos) {
|
||||||
|
var l = 0;
|
||||||
|
var size = 0;
|
||||||
|
var byte_count = 0;
|
||||||
|
var mask = 0x7f;
|
||||||
|
var stop_flag = 0x80;
|
||||||
|
var shift = 0;
|
||||||
|
for (var i = 0; ; i++) {
|
||||||
|
var byte = data[pos];
|
||||||
|
size |= (byte & mask) << shift;
|
||||||
|
shift += 7;
|
||||||
|
l += 1;
|
||||||
|
byte_count += 1;
|
||||||
|
pos -= 1;
|
||||||
|
|
||||||
|
var to_stop = byte & stop_flag;
|
||||||
|
if (byte_count >= 4 || to_stop > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [size, l, pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
read_text() {
|
||||||
|
var text_end = this.palm_header.record_count;
|
||||||
|
var buffers = [];
|
||||||
|
for (var i = 1; i <= text_end; i++) {
|
||||||
|
buffers.push(this.read_text_record(i));
|
||||||
|
}
|
||||||
|
var all = combine_uint8array(buffers)
|
||||||
|
return ab2str(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
read_text_record(i) {
|
||||||
|
var flags = this.mobi_header.extra_flags;
|
||||||
|
var begin = this.reclist[i].offset;
|
||||||
|
var end = this.reclist[i + 1].offset;
|
||||||
|
|
||||||
|
var data = new Uint8Array(this.buffer.slice(begin, end))
|
||||||
|
var ex = this.get_record_extrasize(data, flags);
|
||||||
|
|
||||||
|
data = new Uint8Array(this.buffer.slice(begin, end - ex));
|
||||||
|
if (this.palm_header.compression === 2) {
|
||||||
|
var buffer = uncompression_lz77(data);
|
||||||
|
return buffer.shrink();
|
||||||
|
} else {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read_image(idx) {
|
||||||
|
var first_image_idx = this.mobi_header.first_image_idx;
|
||||||
|
var begin = this.reclist[first_image_idx + idx].offset;
|
||||||
|
var end = this.reclist[first_image_idx + idx + 1].offset;
|
||||||
|
var data = new Uint8Array(this.buffer.slice(begin, end))
|
||||||
|
return new Blob([data.buffer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.header = this.load_pdbheader();
|
||||||
|
this.reclist = this.load_reclist();
|
||||||
|
this.load_record0();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_pdbheader() {
|
||||||
|
var header = {};
|
||||||
|
header.name = this.getStr(32);
|
||||||
|
header.attr = this.getUint16();
|
||||||
|
header.version = this.getUint16();
|
||||||
|
header.ctime = this.getUint32();
|
||||||
|
header.mtime = this.getUint32();
|
||||||
|
header.btime = this.getUint32();
|
||||||
|
header.mod_num = this.getUint32();
|
||||||
|
header.appinfo_offset = this.getUint32();
|
||||||
|
header.sortinfo_offset = this.getUint32();
|
||||||
|
header.type = this.getStr(4);
|
||||||
|
header.creator = this.getStr(4);
|
||||||
|
header.uid = this.getUint32();
|
||||||
|
header.next_rec = this.getUint32();
|
||||||
|
header.record_num = this.getUint16();
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_reclist() {
|
||||||
|
var reclist = [];
|
||||||
|
for (var i = 0; i < this.header.record_num; i++) {
|
||||||
|
var record = {};
|
||||||
|
record.offset = this.getUint32();
|
||||||
|
// TODO(zz) change
|
||||||
|
record.attr = this.getUint32();
|
||||||
|
reclist.push(record);
|
||||||
|
}
|
||||||
|
return reclist;
|
||||||
|
}
|
||||||
|
load_record0() {
|
||||||
|
this.palm_header = this.load_record0_header();
|
||||||
|
this.mobi_header = this.load_mobi_header();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_record0_header() {
|
||||||
|
var p_header = {};
|
||||||
|
var first_record = this.reclist[0];
|
||||||
|
this.setoffset(first_record.offset);
|
||||||
|
|
||||||
|
p_header.compression = this.getUint16();
|
||||||
|
this.skip(2);
|
||||||
|
p_header.text_length = this.getUint32();
|
||||||
|
p_header.record_count = this.getUint16();
|
||||||
|
p_header.record_size = this.getUint16();
|
||||||
|
p_header.encryption_type = this.getUint16();
|
||||||
|
this.skip(2);
|
||||||
|
|
||||||
|
return p_header;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_mobi_header() {
|
||||||
|
var mobi_header = {};
|
||||||
|
|
||||||
|
var start_offset = this.offset;
|
||||||
|
|
||||||
|
mobi_header.identifier = this.getUint32();
|
||||||
|
mobi_header.header_length = this.getUint32();
|
||||||
|
mobi_header.mobi_type = this.getUint32();
|
||||||
|
mobi_header.text_encoding = this.getUint32();
|
||||||
|
mobi_header.uid = this.getUint32();
|
||||||
|
mobi_header.generator_version = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(40);
|
||||||
|
|
||||||
|
mobi_header.first_nonbook_index = this.getUint32();
|
||||||
|
mobi_header.full_name_offset = this.getUint32();
|
||||||
|
mobi_header.full_name_length = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.language = this.getUint32();
|
||||||
|
mobi_header.input_language = this.getUint32();
|
||||||
|
mobi_header.output_language = this.getUint32();
|
||||||
|
mobi_header.min_version = this.getUint32();
|
||||||
|
mobi_header.first_image_idx = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.huff_rec_index = this.getUint32();
|
||||||
|
mobi_header.huff_rec_count = this.getUint32();
|
||||||
|
mobi_header.datp_rec_index = this.getUint32();
|
||||||
|
mobi_header.datp_rec_count = this.getUint32();
|
||||||
|
|
||||||
|
mobi_header.exth_flags = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(36);
|
||||||
|
|
||||||
|
mobi_header.drm_offset = this.getUint32();
|
||||||
|
mobi_header.drm_count = this.getUint32();
|
||||||
|
mobi_header.drm_size = this.getUint32();
|
||||||
|
mobi_header.drm_flags = this.getUint32();
|
||||||
|
|
||||||
|
this.skip(8);
|
||||||
|
|
||||||
|
// TODO (zz) fdst_index
|
||||||
|
this.skip(4);
|
||||||
|
|
||||||
|
this.skip(46);
|
||||||
|
|
||||||
|
mobi_header.extra_flags = this.getUint16();
|
||||||
|
|
||||||
|
this.setoffset(start_offset + mobi_header.header_length)
|
||||||
|
|
||||||
|
return mobi_header;
|
||||||
|
}
|
||||||
|
load_exth_header() {
|
||||||
|
// TODO
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render_to(id) {
|
||||||
|
this.load();
|
||||||
|
var content = this.read_text();
|
||||||
|
|
||||||
|
var bookDom = document.getElementById(id);
|
||||||
|
while (bookDom.firstChild) {
|
||||||
|
bookDom.removeChild(bookDom.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bookDoc = domParser.parseFromString(content, "text/html");
|
||||||
|
bookDoc.body.childNodes.forEach(function (x) {
|
||||||
|
if (x instanceof Element) {
|
||||||
|
bookDom.appendChild(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var imgDoms = bookDom.getElementsByTagName("img");
|
||||||
|
for (var i = 0; i < imgDoms.length; i++) {
|
||||||
|
this.render_image(imgDoms, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render_image(imgDoms, i) {
|
||||||
|
var imgDom = imgDoms[i];
|
||||||
|
var idx = +imgDom.getAttribute("recindex");
|
||||||
|
var blob = this.read_image(idx - 1);
|
||||||
|
var imgReader = new FileReader();
|
||||||
|
imgReader.onload = function (e) {
|
||||||
|
imgDom.src = e.target.result;
|
||||||
|
};
|
||||||
|
imgReader.readAsDataURL(blob);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user