Bug 23566: Continue on device - with QR codes
This patch adds the option to show a QR code on the OPAC bibliographic detail page. The URL of the page is encoded in the image so that scanning it will take the user to that page on their device. The feature is controlled by a new system preference, OPACDetailQRCode, which is disabled by default. The QR Code is generated by a JavaScript library, "kjua" (https://github.com/lrsjng/kjua), which has been added to the "About" page in the staff client. To test, apply the patch and run the database update. Rebuild the OPAC CSS (https://wiki.koha-community.org/wiki/Working_with_SCSS_in_the_OPAC_and_staff_client). - In the staff client, go to Administration -> System preferences. - Locate the OPACDetailQRCode system preferences under OPAC -> Features. It should be disabled. - Enable the preference and switch to the OPAC. - Locate a title in the catalog and view the detail page. - In the sidebar menu there should be a "Send to device" link. - Clicking the link should display a QR Code. - Scan the code using a QR Code-capable device. - The URL should be correct. - Disable the system preference and confirm that the "Send to device" link no longer appears on the OPAC detail page. Signed-off-by: Christopher Brannon <cbrannon@cdalibrary.org> Signed-off-by: Nick Clemens <nick@bywatersolutions.com> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
This commit is contained in:
parent
f2b8444267
commit
4c17151d74
15 changed files with 399 additions and 0 deletions
11
installer/data/mysql/atomicupdate/bug_23566.perl
Normal file
11
installer/data/mysql/atomicupdate/bug_23566.perl
Normal file
|
@ -0,0 +1,11 @@
|
|||
$DBversion = 'XXX'; # will be replaced by the RM
|
||||
|
||||
if( CheckVersion( $DBversion ) ) {
|
||||
$dbh->do(q|
|
||||
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES
|
||||
('OPACDetailQRCode','0','','Enable the display of a QR Code on the OPAC detail page','YesNo');
|
||||
|);
|
||||
|
||||
SetVersion( $DBversion );
|
||||
print "Upgrade to $DBversion done (Bug 23566 - Add OPACDetailQRCode system preference)\n";
|
||||
}
|
|
@ -701,6 +701,9 @@
|
|||
|
||||
<h2>HC Sticky</h2>
|
||||
<p><a href="http://somewebmedia.com/hc-sticky/">HC Sticky</a> by Some Web Media is a JavaScript library that makes any element on your page visible while you scroll, licensed under the <a href="https://github.com/somewebmedia/hc-sticky/blob/master/LICENSE">MIT license</a>.</p>
|
||||
|
||||
<h2>kjua</h2>
|
||||
<p><a href="https://larsjung.de/kjua/">kjua</a> by Lars Jung is a JavaScript library that generates QR codes, licensed under the <a href="https://github.com/lrsjng/kjua/blob/master/README.md">MIT license</a>.</p>
|
||||
</div>
|
||||
|
||||
<div id="translations">
|
||||
|
|
|
@ -402,6 +402,12 @@ OPAC:
|
|||
yes: Show
|
||||
no: "Don't show"
|
||||
- patron images on the patron information page in the OPAC.
|
||||
-
|
||||
- pref: OPACDetailQRCode
|
||||
choices:
|
||||
yes: Enable
|
||||
no: "Don't enable"
|
||||
- the option to show a QR Code on the OPAC bibliographic detail page.
|
||||
-
|
||||
- pref: OPACFinesTab
|
||||
choices:
|
||||
|
|
|
@ -259,6 +259,12 @@ a {
|
|||
&.removefromlist {
|
||||
@extend %initial_icon;
|
||||
}
|
||||
|
||||
&.show_qrcode {
|
||||
@extend %initial_icon;
|
||||
background-position: 0 -1164px; /* QR Code */
|
||||
padding-left: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -3210,5 +3216,13 @@ $star-selected: #EDB867;
|
|||
|
||||
/* END jQuery Bar Rating plugin for star ratings */
|
||||
|
||||
#qrcode {
|
||||
margin-left: 35px;
|
||||
|
||||
img,
|
||||
canvas {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@import "responsive";
|
||||
|
|
|
@ -40,6 +40,14 @@
|
|||
</li>
|
||||
[% END %]
|
||||
|
||||
|
||||
[% IF ( Koha.Preference('OPACDetailQRCode' ) ) %]
|
||||
<li>
|
||||
<a class="show_qrcode" href="#">Send to device</a>
|
||||
<div id="qrcode" class="hidden"></div>
|
||||
</li>
|
||||
[% END %]
|
||||
|
||||
[% SET export_options = Koha.Preference('OpacExportOptions').split(',') %]
|
||||
[% IF export_options.size %]
|
||||
<li>
|
||||
|
|
|
@ -1392,6 +1392,9 @@
|
|||
[% IF ( OpacStarRatings != 'disable' ) %][% Asset.js("lib/jquery/plugins/jquery.barrating.min.js") | $raw %][% END %]
|
||||
|
||||
[% IF ( OpacHighlightedWords ) %][% Asset.js("lib/jquery/plugins/jquery.highlight-3.js") | $raw %][% END %]
|
||||
[% IF ( Koha.Preference('OPACDetailQRCode') ) %]
|
||||
[% Asset.js("lib/kjua/kjua.min.js") | $raw %]
|
||||
[% END %]
|
||||
|
||||
<script>
|
||||
[% IF ( OpacHighlightedWords ) %]
|
||||
|
@ -1420,6 +1423,30 @@
|
|||
[% END %]
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
[% IF ( Koha.Preference('OPACDetailQRCode') ) %]
|
||||
|
||||
var qrcode = kjua({
|
||||
ecLevel: "H",
|
||||
render: "canvas",
|
||||
rounded: 100,
|
||||
size: 150,
|
||||
text: location.href,
|
||||
});
|
||||
if (qrcode) {
|
||||
document.getElementById("qrcode").appendChild( qrcode );
|
||||
}
|
||||
|
||||
$(".show_qrcode").on("click", function(){
|
||||
var qrcodeImg = $("#qrcode");
|
||||
if( qrcodeImg.hasClass("hidden") ){
|
||||
qrcodeImg.removeClass("hidden");
|
||||
} else {
|
||||
qrcodeImg.addClass("hidden");
|
||||
}
|
||||
});
|
||||
[% END %]
|
||||
|
||||
$('#bibliodescriptions').tabs();
|
||||
$(".branch-info-tooltip-trigger").uitooltip({
|
||||
position: { my: "left+15 center", at: "right center" },
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 8.1 KiB |
18
koha-tmpl/opac-tmpl/lib/kjua/kjua.js
Normal file
18
koha-tmpl/opac-tmpl/lib/kjua/kjua.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
const {create_canvas, canvas_to_img, dpr} = require('./lib/dom');
|
||||
const defaults = require('./lib/defaults');
|
||||
const qrcode = require('./lib/qrcode');
|
||||
const draw = require('./lib/draw');
|
||||
|
||||
module.exports = options => {
|
||||
const settings = Object.assign({}, defaults, options);
|
||||
|
||||
const qr = qrcode(settings.text, settings.ecLevel, settings.minVersion, settings.quiet);
|
||||
const ratio = settings.ratio || dpr;
|
||||
const canvas = create_canvas(settings.size, ratio);
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
context.scale(ratio, ratio);
|
||||
draw(qr, context, settings);
|
||||
|
||||
return settings.render === 'image' ? canvas_to_img(canvas) : canvas;
|
||||
};
|
2
koha-tmpl/opac-tmpl/lib/kjua/kjua.min.js
vendored
Normal file
2
koha-tmpl/opac-tmpl/lib/kjua/kjua.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
50
koha-tmpl/opac-tmpl/lib/kjua/lib/defaults.js
Normal file
50
koha-tmpl/opac-tmpl/lib/kjua/lib/defaults.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
module.exports = {
|
||||
// render method: 'canvas' or 'image'
|
||||
render: 'image',
|
||||
|
||||
// render pixel-perfect lines
|
||||
crisp: true,
|
||||
|
||||
// minimum version: 1..40
|
||||
minVersion: 1,
|
||||
|
||||
// error correction level: 'L', 'M', 'Q' or 'H'
|
||||
ecLevel: 'L',
|
||||
|
||||
// size in pixel
|
||||
size: 200,
|
||||
|
||||
// pixel-ratio, null for devicePixelRatio
|
||||
ratio: null,
|
||||
|
||||
// code color
|
||||
fill: '#333',
|
||||
|
||||
// background color
|
||||
back: '#fff',
|
||||
|
||||
// content
|
||||
text: 'no text',
|
||||
|
||||
// roundend corners in pc: 0..100
|
||||
rounded: 0,
|
||||
|
||||
// quiet zone in modules
|
||||
quiet: 0,
|
||||
|
||||
// modes: 'plain', 'label' or 'image'
|
||||
mode: 'plain',
|
||||
|
||||
// label/image size and pos in pc: 0..100
|
||||
mSize: 30,
|
||||
mPosX: 50,
|
||||
mPosY: 50,
|
||||
|
||||
// label
|
||||
label: 'no label',
|
||||
fontname: 'sans',
|
||||
fontcolor: '#333',
|
||||
|
||||
// image element
|
||||
image: null
|
||||
};
|
32
koha-tmpl/opac-tmpl/lib/kjua/lib/dom.js
Normal file
32
koha-tmpl/opac-tmpl/lib/kjua/lib/dom.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
const win = global.window;
|
||||
const doc = win.document;
|
||||
const dpr = win.devicePixelRatio || 1;
|
||||
const create = name => doc.createElement(name);
|
||||
const get_attr = (el, key) => el.getAttribute(key);
|
||||
const set_attr = (el, key, value) => el.setAttribute(key, value);
|
||||
|
||||
const create_canvas = (size, ratio) => {
|
||||
const canvas = create('canvas');
|
||||
set_attr(canvas, 'width', size * ratio);
|
||||
set_attr(canvas, 'height', size * ratio);
|
||||
canvas.style.width = `${size}px`;
|
||||
canvas.style.height = `${size}px`;
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const canvas_to_img = canvas => {
|
||||
const img = create('img');
|
||||
set_attr(img, 'crossorigin', 'anonymous');
|
||||
set_attr(img, 'src', canvas.toDataURL('image/png'));
|
||||
set_attr(img, 'width', get_attr(canvas, 'width'));
|
||||
set_attr(img, 'height', get_attr(canvas, 'height'));
|
||||
img.style.width = canvas.style.width;
|
||||
img.style.height = canvas.style.height;
|
||||
return img;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create_canvas,
|
||||
canvas_to_img,
|
||||
dpr
|
||||
};
|
48
koha-tmpl/opac-tmpl/lib/kjua/lib/draw.js
Normal file
48
koha-tmpl/opac-tmpl/lib/kjua/lib/draw.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const draw_module_rounded = require('./draw_rounded');
|
||||
const draw_mode = require('./draw_mode');
|
||||
|
||||
const draw_background = (ctx, settings) => {
|
||||
ctx.fillStyle = settings.back;
|
||||
ctx.fillRect(0, 0, settings.size, settings.size);
|
||||
};
|
||||
|
||||
const draw_module_default = (qr, ctx, settings, width, row, col) => {
|
||||
if (qr.isDark(row, col)) {
|
||||
ctx.rect(col * width, row * width, width, width);
|
||||
}
|
||||
};
|
||||
|
||||
const draw_modules = (qr, ctx, settings) => {
|
||||
if (!qr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draw_module = settings.rounded > 0 && settings.rounded <= 100 ? draw_module_rounded : draw_module_default;
|
||||
const mod_count = qr.moduleCount;
|
||||
|
||||
let mod_size = settings.size / mod_count;
|
||||
let offset = 0;
|
||||
if (settings.crisp) {
|
||||
mod_size = Math.floor(mod_size);
|
||||
offset = Math.floor((settings.size - mod_size * mod_count) / 2);
|
||||
}
|
||||
|
||||
ctx.translate(offset, offset);
|
||||
ctx.beginPath();
|
||||
for (let row = 0; row < mod_count; row += 1) {
|
||||
for (let col = 0; col < mod_count; col += 1) {
|
||||
draw_module(qr, ctx, settings, mod_size, row, col);
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = settings.fill;
|
||||
ctx.fill();
|
||||
ctx.translate(-offset, -offset);
|
||||
};
|
||||
|
||||
const draw = (qr, ctx, settings) => {
|
||||
draw_background(ctx, settings);
|
||||
draw_modules(qr, ctx, settings);
|
||||
draw_mode(ctx, settings);
|
||||
};
|
||||
|
||||
module.exports = draw;
|
47
koha-tmpl/opac-tmpl/lib/kjua/lib/draw_mode.js
Normal file
47
koha-tmpl/opac-tmpl/lib/kjua/lib/draw_mode.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const draw_label = (ctx, settings) => {
|
||||
const size = settings.size;
|
||||
const font = 'bold ' + settings.mSize * 0.01 * size + 'px ' + settings.fontname;
|
||||
|
||||
ctx.strokeStyle = settings.back;
|
||||
ctx.lineWidth = settings.mSize * 0.01 * size * 0.1;
|
||||
ctx.fillStyle = settings.fontcolor;
|
||||
ctx.font = font;
|
||||
|
||||
const w = ctx.measureText(settings.label).width;
|
||||
const sh = settings.mSize * 0.01;
|
||||
const sw = w / size;
|
||||
const sl = (1 - sw) * settings.mPosX * 0.01;
|
||||
const st = (1 - sh) * settings.mPosY * 0.01;
|
||||
const x = sl * size;
|
||||
const y = st * size + 0.75 * settings.mSize * 0.01 * size;
|
||||
|
||||
ctx.strokeText(settings.label, x, y);
|
||||
ctx.fillText(settings.label, x, y);
|
||||
};
|
||||
|
||||
const draw_image = (ctx, settings) => {
|
||||
const size = settings.size;
|
||||
const w = settings.image.naturalWidth || 1;
|
||||
const h = settings.image.naturalHeight || 1;
|
||||
const sh = settings.mSize * 0.01;
|
||||
const sw = sh * w / h;
|
||||
const sl = (1 - sw) * settings.mPosX * 0.01;
|
||||
const st = (1 - sh) * settings.mPosY * 0.01;
|
||||
const x = sl * size;
|
||||
const y = st * size;
|
||||
const iw = sw * size;
|
||||
const ih = sh * size;
|
||||
|
||||
ctx.drawImage(settings.image, x, y, iw, ih);
|
||||
};
|
||||
|
||||
const draw_mode = (ctx, settings) => {
|
||||
const mode = settings.mode;
|
||||
if (mode === 'label') {
|
||||
draw_label(ctx, settings);
|
||||
} else if (mode === 'image') {
|
||||
draw_image(ctx, settings);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = draw_mode;
|
91
koha-tmpl/opac-tmpl/lib/kjua/lib/draw_rounded.js
Normal file
91
koha-tmpl/opac-tmpl/lib/kjua/lib/draw_rounded.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
const wrap_ctx = ctx => {
|
||||
return {
|
||||
c: ctx,
|
||||
m(...args) {this.c.moveTo(...args); return this;},
|
||||
l(...args) {this.c.lineTo(...args); return this;},
|
||||
a(...args) {this.c.arcTo(...args); return this;}
|
||||
};
|
||||
};
|
||||
|
||||
const draw_dark = (ctx, l, t, r, b, rad, nw, ne, se, sw) => {
|
||||
if (nw) {
|
||||
ctx.m(l + rad, t);
|
||||
} else {
|
||||
ctx.m(l, t);
|
||||
}
|
||||
|
||||
if (ne) {
|
||||
ctx.l(r - rad, t).a(r, t, r, b, rad);
|
||||
} else {
|
||||
ctx.l(r, t);
|
||||
}
|
||||
|
||||
if (se) {
|
||||
ctx.l(r, b - rad).a(r, b, l, b, rad);
|
||||
} else {
|
||||
ctx.l(r, b);
|
||||
}
|
||||
|
||||
if (sw) {
|
||||
ctx.l(l + rad, b).a(l, b, l, t, rad);
|
||||
} else {
|
||||
ctx.l(l, b);
|
||||
}
|
||||
|
||||
if (nw) {
|
||||
ctx.l(l, t + rad).a(l, t, r, t, rad);
|
||||
} else {
|
||||
ctx.l(l, t);
|
||||
}
|
||||
};
|
||||
|
||||
const draw_light = (ctx, l, t, r, b, rad, nw, ne, se, sw) => {
|
||||
if (nw) {
|
||||
ctx.m(l + rad, t).l(l, t).l(l, t + rad).a(l, t, l + rad, t, rad);
|
||||
}
|
||||
|
||||
if (ne) {
|
||||
ctx.m(r - rad, t).l(r, t).l(r, t + rad).a(r, t, r - rad, t, rad);
|
||||
}
|
||||
|
||||
if (se) {
|
||||
ctx.m(r - rad, b).l(r, b).l(r, b - rad).a(r, b, r - rad, b, rad);
|
||||
}
|
||||
|
||||
if (sw) {
|
||||
ctx.m(l + rad, b).l(l, b).l(l, b - rad).a(l, b, l + rad, b, rad);
|
||||
}
|
||||
};
|
||||
|
||||
const draw_mod = (qr, ctx, settings, width, row, col) => {
|
||||
const left = col * width;
|
||||
const top = row * width;
|
||||
const right = left + width;
|
||||
const bottom = top + width;
|
||||
const radius = settings.rounded * 0.005 * width;
|
||||
|
||||
const isDark = qr.isDark;
|
||||
const rowT = row - 1;
|
||||
const rowB = row + 1;
|
||||
const colL = col - 1;
|
||||
const colR = col + 1;
|
||||
const dC = isDark(row, col);
|
||||
const dNW = isDark(rowT, colL);
|
||||
const dN = isDark(rowT, col);
|
||||
const dNE = isDark(rowT, colR);
|
||||
const dE = isDark(row, colR);
|
||||
const dSE = isDark(rowB, colR);
|
||||
const dS = isDark(rowB, col);
|
||||
const dSW = isDark(rowB, colL);
|
||||
const dW = isDark(row, colL);
|
||||
|
||||
ctx = wrap_ctx(ctx);
|
||||
|
||||
if (dC) {
|
||||
draw_dark(ctx, left, top, right, bottom, radius, !dN && !dW, !dN && !dE, !dS && !dE, !dS && !dW);
|
||||
} else {
|
||||
draw_light(ctx, left, top, right, bottom, radius, dN && dW && dNW, dN && dE && dNE, dS && dE && dSE, dS && dW && dSW);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = draw_mod;
|
42
koha-tmpl/opac-tmpl/lib/kjua/lib/qrcode.js
Normal file
42
koha-tmpl/opac-tmpl/lib/kjua/lib/qrcode.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
const RE_CODE_LENGTH_OVERFLOW = /code length overflow/i;
|
||||
|
||||
const qr_gen = require('qrcode-generator');
|
||||
qr_gen.stringToBytes = qr_gen.stringToBytesFuncs['UTF-8'];
|
||||
|
||||
const min_qrcode = (text, level, min_ver = 1) => {
|
||||
min_ver = Math.max(1, min_ver);
|
||||
|
||||
for (let version = min_ver; version <= 40; version += 1) {
|
||||
try {
|
||||
const qr = qr_gen(version, level);
|
||||
qr.addData(text);
|
||||
qr.make();
|
||||
const moduleCount = qr.getModuleCount();
|
||||
const isDark = (row, col) => {
|
||||
return row >= 0 &&
|
||||
row < moduleCount &&
|
||||
col >= 0 &&
|
||||
col < moduleCount &&
|
||||
qr.isDark(row, col);
|
||||
};
|
||||
return {text, level, version, moduleCount, isDark};
|
||||
} catch (err) {
|
||||
if (!(version < 40 && RE_CODE_LENGTH_OVERFLOW.test(err))) {
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const quiet_qrcode = (text = '', level = 'L', min_ver = 1, quiet = 0) => {
|
||||
const qr = min_qrcode(text, level, min_ver);
|
||||
if (qr) {
|
||||
const prev_is_dark = qr.isDark;
|
||||
qr.moduleCount += 2 * quiet;
|
||||
qr.isDark = (row, col) => prev_is_dark(row - quiet, col - quiet);
|
||||
}
|
||||
return qr;
|
||||
};
|
||||
|
||||
module.exports = quiet_qrcode;
|
Loading…
Reference in a new issue