im_reader 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -3
- data/app/assets/javascripts/im_reader/reader.js +42 -60
- data/app/controllers/im_reader/epub_reader_controller.rb +24 -14
- data/app/views/im_reader/epub_reader/show.html.erb +1 -0
- data/config/locales/de.yml +3 -0
- data/config/locales/en.yml +3 -0
- data/config/locales/es.yml +3 -0
- data/config/locales/fr.yml +3 -0
- data/config/locales/pt.yml +3 -0
- data/lib/im_reader/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9df80b54a543f446c9a12e0f09d296813cb861cd44f1ed283d5b30e3df57797c
|
|
4
|
+
data.tar.gz: 48e211b702f2dd7e3261db75d8be8d48c7c759e0dd7c23b85fdaa63148f0ad08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df2f8086643d74000d8fed67ad0f0068b56966757c4de95fe72ed38f4f8a113486294f0240a16c95d353b6cd3b0784b3313de5188e18a7ce4b10d02cd9959788
|
|
7
|
+
data.tar.gz: 066ed3cb0d3ead6971f51589aa722c872b77f9394d107246e89b1b9cb02d74aacbb5446d5247d4e53e3fafab02ce7103914db1ea8e8e3010b020a9dac875db64
|
data/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# im_reader
|
|
2
2
|
|
|
3
|
-
**im_reader** is a mountable Ruby on Rails engine that
|
|
4
|
-
It
|
|
3
|
+
**im_reader** is a mountable Ruby on Rails engine that encapsulates the [`epub.js`](https://github.com/intity/epub-js) JavaScript library to provide an elegant in-browser EPUB reader.
|
|
4
|
+
It integrates [`Semantic UI`](https://github.com/Semantic-Org/Semantic-UI) for the user interface styling, offering a modern and responsive reading experience.
|
|
5
|
+
Designed to fit seamlessly into any Rails application, it allows users to read `.epub` files hosted remotely, with a built-in table of contents, cover display, and customizable UI labels.
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -48,7 +49,7 @@ This will expose the following route inside your application:
|
|
|
48
49
|
### Example usage
|
|
49
50
|
|
|
50
51
|
```
|
|
51
|
-
/reader/
|
|
52
|
+
/reader/epub?url=https://your-server.com/path/to/book.epub
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
This endpoint will download the EPUB file from the given URL, stream it securely to the client, and display it in the embedded reader.
|
|
@@ -76,6 +77,7 @@ fr:
|
|
|
76
77
|
start: "Commencer la lecture"
|
|
77
78
|
messages:
|
|
78
79
|
loading: "Chargement du livre ..."
|
|
80
|
+
reading_error: "Impossible de récupérer l’EPUB"
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
---
|
|
@@ -108,3 +110,12 @@ http://localhost:3000/reader/epub?url=https://your-server.com/path/to/book.epub
|
|
|
108
110
|
|
|
109
111
|
This project is distributed under the MIT License.
|
|
110
112
|
See the `LICENSE` file for more details.
|
|
113
|
+
|
|
114
|
+
## Credits
|
|
115
|
+
|
|
116
|
+
- **EPUB rendering:** [epub.js (Intity fork)](https://github.com/intity/epub-js)
|
|
117
|
+
Licensed under the BSD 2-Clause License.
|
|
118
|
+
- **UI framework:** [Semantic UI](https://github.com/Semantic-Org/Semantic-UI)
|
|
119
|
+
Licensed under the MIT License.
|
|
120
|
+
|
|
121
|
+
**im_reader** integrates and extends these open-source projects to provide a smooth EPUB reading experience inside Ruby on Rails.
|
|
@@ -19,17 +19,43 @@
|
|
|
19
19
|
$("#reader-root").append(overlay);
|
|
20
20
|
|
|
21
21
|
var bookUrl = $root.data("book-url");
|
|
22
|
+
if (!bookUrl) {
|
|
23
|
+
$("#cover-content").html(`<p class="ui red text">${i18n.missing_url || "URL manquante."}</p>`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
var theme = "light";
|
|
23
27
|
var fontSize = 100;
|
|
28
|
+
fetch(bookUrl, { method: "HEAD" })
|
|
29
|
+
.then((res) => {
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
// Si HEAD échoue, on tente GET pour récupérer le message texte
|
|
32
|
+
return fetch(bookUrl).then(async (resp) => {
|
|
33
|
+
const msg = await resp.text();
|
|
34
|
+
throw new Error(msg || i18n.reading_error || "Erreur de lecture.");
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.then(() => startReader(bookUrl, i18n, overlay))
|
|
39
|
+
.catch((err) => {
|
|
40
|
+
console.error("[im_reader] load error:", err);
|
|
41
|
+
$("#cover-content").html(`<p class="ui red text">${err.message}</p>`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
24
44
|
|
|
45
|
+
function startReader(bookUrl, i18n, overlay) {
|
|
25
46
|
var $viewer = $("#viewer"), $toc = $("#toc");
|
|
26
|
-
var $prev_button = $("#prev_button"), $next_button = $("#next_button")
|
|
47
|
+
var $prev_button = $("#prev_button"), $next_button = $("#next_button");
|
|
27
48
|
|
|
28
|
-
if (!window.ePub) {
|
|
49
|
+
if (!window.ePub) {
|
|
50
|
+
$("#cover-content").html(`<p class="ui red text">ePub introuvable</p>`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
29
53
|
|
|
30
|
-
window.book = window.ePub(bookUrl, {
|
|
54
|
+
window.book = window.ePub(bookUrl, {
|
|
55
|
+
openAs: "epub",
|
|
31
56
|
replacements: "blob",
|
|
32
|
-
restore: false
|
|
57
|
+
restore: false
|
|
58
|
+
});
|
|
33
59
|
|
|
34
60
|
var rendition = book.renderTo("viewer", {
|
|
35
61
|
width: "100%",
|
|
@@ -53,34 +79,17 @@
|
|
|
53
79
|
if (iframe) iframe.setAttribute("sandbox", "allow-same-origin allow-scripts");
|
|
54
80
|
});
|
|
55
81
|
|
|
56
|
-
$prev_button.on("click", () =>
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
$next_button.on("click", () => {
|
|
61
|
-
rendition.next();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
rendition.on("relocated", (location) => {
|
|
65
|
-
if (book.locations.total > 0) {
|
|
66
|
-
const current = book.locations.locationFromCfi(location.start.cfi);
|
|
67
|
-
const total = book.locations.total;
|
|
68
|
-
let percentage = current / total;
|
|
69
|
-
if (location.atEnd) percentage = 1.0;
|
|
70
|
-
|
|
71
|
-
$('#progressBar').progress('set percent', Math.round(percentage * 100));
|
|
72
|
-
}
|
|
73
|
-
});
|
|
82
|
+
$prev_button.on("click", () => rendition.prev());
|
|
83
|
+
$next_button.on("click", () => rendition.next());
|
|
74
84
|
|
|
75
85
|
book.ready.then(() => Promise.all([book.loaded.navigation, book.loaded.manifest]))
|
|
76
86
|
.then(([toc, manifest]) => {
|
|
77
|
-
|
|
78
87
|
book.coverUrl().then((url) => {
|
|
79
88
|
if (url) {
|
|
80
89
|
$("#cover-content").html(`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
<img src="${url}" alt="${i18n.book_cover}">
|
|
91
|
+
<button id="start_button" class="ui primary button">${i18n.start}</button>
|
|
92
|
+
`);
|
|
84
93
|
|
|
85
94
|
overlay.on("click", "#start_button", () => {
|
|
86
95
|
overlay.remove();
|
|
@@ -93,51 +102,24 @@
|
|
|
93
102
|
});
|
|
94
103
|
|
|
95
104
|
const $toc = $("#toc");
|
|
96
|
-
|
|
97
|
-
function normalizeHref(href) {
|
|
98
|
-
for (const key in manifest) {
|
|
99
|
-
const entry = manifest[key];
|
|
100
|
-
if (
|
|
101
|
-
entry.href.endsWith(href) ||
|
|
102
|
-
entry.href.endsWith("x" + href) ||
|
|
103
|
-
entry.href.endsWith("/" + href)
|
|
104
|
-
) {
|
|
105
|
-
return entry.href;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Par défaut
|
|
110
|
-
return href;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
105
|
toc.toc.forEach((chapter) => {
|
|
114
|
-
const resolvedHref = normalizeHref(chapter.href);
|
|
115
|
-
|
|
116
106
|
$("<a>")
|
|
117
107
|
.addClass("item")
|
|
118
108
|
.attr("href", "#")
|
|
119
109
|
.text(chapter.label)
|
|
120
110
|
.on("click", (e) => {
|
|
121
111
|
e.preventDefault();
|
|
122
|
-
rendition
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
console.error("[im_reader] Display error:", err.message, resolvedHref);
|
|
126
|
-
});
|
|
112
|
+
rendition.display(chapter.href).catch((err) => {
|
|
113
|
+
console.error("[im_reader] Display error:", err.message);
|
|
114
|
+
});
|
|
127
115
|
})
|
|
128
116
|
.appendTo($toc);
|
|
129
117
|
});
|
|
130
118
|
})
|
|
131
|
-
.catch((err) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}).then(() => {
|
|
136
|
-
}).catch(e => console.error("Error, book not ready", e));
|
|
137
|
-
|
|
138
|
-
rendition.on("displayError", (e)=> console.error("Error on display", e));
|
|
139
|
-
|
|
140
|
-
rendition.display().catch(e=>console.error("Error while rendering book", e));
|
|
119
|
+
.catch((err) => {
|
|
120
|
+
console.error("[im_reader] TOC load error:", err);
|
|
121
|
+
$("#cover-content").html(`<p class="ui red text">${i18n.reading_error || "Erreur de lecture du livre."}</p>`);
|
|
122
|
+
});
|
|
141
123
|
}
|
|
142
124
|
|
|
143
125
|
$(function(){ var $root = $("#reader-root"); if ($root.length) initReader($root); });
|
|
@@ -4,37 +4,47 @@ module ImReader
|
|
|
4
4
|
|
|
5
5
|
def show
|
|
6
6
|
@remote_url = params[:url]
|
|
7
|
-
puts "#### PLOP ####"
|
|
8
7
|
@url = im_reader.remote_epub_reader_url(url: @remote_url)
|
|
9
|
-
puts "***** OK *****"
|
|
10
8
|
end
|
|
11
9
|
|
|
12
10
|
def remote
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
uri = URI.parse(url)
|
|
18
|
-
response = fetch_with_redirect(uri)
|
|
11
|
+
raw_url = params[:url].to_s.strip
|
|
12
|
+
return render plain: I18n.t('im_reader.messages.missing_url'), status: 400 if raw_url.empty?
|
|
13
|
+
uri = parse_uri(raw_url)
|
|
14
|
+
return render plain: I18n.t('im_reader.messages.invalid_url'), status: 400 unless uri
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
puts "***** REMOTE 2 *****"
|
|
16
|
+
response = fetch_with_redirect(uri)
|
|
22
17
|
|
|
23
18
|
if response.is_a?(Net::HTTPSuccess)
|
|
24
|
-
|
|
25
|
-
puts "***** REMOTE 3.1 *****"
|
|
26
19
|
send_data response.body,
|
|
27
20
|
filename: File.basename(uri.path.presence || "remote.epub"),
|
|
28
21
|
type: "application/epub+zip",
|
|
29
22
|
disposition: "inline"
|
|
30
23
|
else
|
|
31
|
-
|
|
32
|
-
render plain: "Impossible de récupérer l’EPUB", status: 404
|
|
24
|
+
render plain: I18n.t('im_reader.messages.reading_error'), status: 404
|
|
33
25
|
end
|
|
34
26
|
end
|
|
35
27
|
|
|
36
28
|
private
|
|
37
29
|
|
|
30
|
+
def parse_uri(value)
|
|
31
|
+
raw = value.to_s.strip
|
|
32
|
+
return nil if raw.empty?
|
|
33
|
+
|
|
34
|
+
candidates = [raw, CGI.unescape(raw)].uniq
|
|
35
|
+
|
|
36
|
+
candidates.each do |c|
|
|
37
|
+
begin
|
|
38
|
+
uri = URI.parse(c)
|
|
39
|
+
return uri if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
40
|
+
rescue URI::InvalidURIError, URI::InvalidComponentError
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
38
48
|
def fetch_with_redirect(uri, limit = 5)
|
|
39
49
|
raise "Too many redirects" if limit == 0
|
|
40
50
|
|
data/config/locales/de.yml
CHANGED
data/config/locales/en.yml
CHANGED
data/config/locales/es.yml
CHANGED
data/config/locales/fr.yml
CHANGED
data/config/locales/pt.yml
CHANGED
data/lib/im_reader/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: im_reader
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Elodie Ailleaume
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sass
|