Commit 59711946 authored by Ozzieisaacs's avatar Ozzieisaacs

Merge remote-tracking branch 'audiobook/Branch_c27805b4'

# Conflicts:
#	cps/templates/detail.html
#	cps/web.py
#	readme.md
parent d81cb292
SVG icons via Icomoon
https://icomoon.io/app
Icons used from the following sets:
* Entypo - Creative Commons BY-SA 3.0 http://creativecommons.org/licenses/by-sa/3.0/us/
* IcoMoon - Free (GPL) http://www.gnu.org/licenses/gpl.html
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M20.002 7.75h-13.5v-2.75l-5.25 4.5 5.25 4.5v-2.75h12.5v5.25h-16.25v3.5h17.25c1.38 0 2.5-1.12 2.5-2.5v-7.25c0-1.381-1.121-2.5-2.5-2.5z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M5 21.25c0 1.38 1.12 2.5 2.5 2.5 1.381 0 2.5-1.12 2.5-2.5s-1.119-2.5-2.5-2.5c-1.38 0-2.5 1.12-2.5 2.5zM17.5 21.25c0 1.38 1.12 2.5 2.5 2.5 1.381 0 2.5-1.12 2.5-2.5s-1.119-2.5-2.5-2.5c-1.38 0-2.5 1.12-2.5 2.5zM9.434 15.34l13.836-3.952c0.264-0.076 0.48-0.363 0.48-0.638v-6.875h-17.625v-2.125c0-0.275-0.224-0.5-0.5-0.5h-3.875c-0.275 0-0.5 0.225-0.5 0.5v2h2.429l2.46 11.321 0.238 1.179v1.875c0 0.275 0.225 0.5 0.499 0.5h16.376c0.274 0 0.499-0.225 0.499-0.5v-1.875h-14.060c-1.436 0-1.466-0.562-0.256-0.91z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M9.356 12.5c0 0.323 0.327 0.552 0.327 0.552l9.094 5.711c0.674 0.441 1.223 0.116 1.223-0.719v-11.090c0-0.836-0.549-1.161-1.223-0.72l-9.094 5.713c0 0.001-0.327 0.23-0.327 0.552zM5 6.696v11.607c0 1.11 0.765 1.45 1.875 1.45s1.875-0.34 1.875-1.449v-11.609c0-1.111-0.765-1.451-1.875-1.451s-1.875 0.341-1.875 1.451z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M15.316 11.948l-9.094-5.713c-0.673-0.44-1.223-0.116-1.223 0.721v11.089c0 0.837 0.55 1.16 1.223 0.72l9.094-5.711c0 0 0.329-0.231 0.329-0.552s-0.329-0.554-0.329-0.554zM18.125 5.245c-1.11 0-1.875 0.341-1.875 1.451v11.607c0 1.111 0.765 1.452 1.875 1.452s1.875-0.341 1.875-1.452v-11.607c0-1.11-0.765-1.451-1.875-1.451z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M6.25 16.25h-1.25c-0.69 0-1.25 0.559-1.25 1.25s0.56 1.25 1.25 1.25h1.25c0.69 0 1.25-0.559 1.25-1.25s-0.56-1.25-1.25-1.25zM6.25 11.25h-1.25c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h1.25c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25zM6.25 6.25h-1.25c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h1.25c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25zM11.25 8.75h8.75c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25h-8.75c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25zM20 11.25h-8.75c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h8.75c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25zM20 16.25h-8.75c-0.69 0-1.25 0.559-1.25 1.25s0.56 1.25 1.25 1.25h8.75c0.691 0 1.25-0.559 1.25-1.25s-0.559-1.25-1.25-1.25z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M20 11.25h-15c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h15c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25zM5 8.75h15c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25h-15c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25zM20 16.25h-15c-0.69 0-1.25 0.559-1.25 1.25s0.56 1.25 1.25 1.25h15c0.691 0 1.25-0.559 1.25-1.25s-0.559-1.25-1.25-1.25z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M21.25 7.75h-4.75v3.5h3.75v5.25h-15.5v-5.25h5.25v2.75l5-4.5-5-4.5v2.75h-6.25c-1.38 0-2.5 1.119-2.5 2.5v7.25c0 1.38 1.12 2.5 2.5 2.5h17.5c1.381 0 2.5-1.12 2.5-2.5v-7.25c0-1.381-1.119-2.5-2.5-2.5z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M18.75 1.25h-12.5c-1.375 0-2.5 1.125-2.5 2.5v17.5c0 1.375 1.125 2.5 2.5 2.5h12.5c1.376 0 2.5-1.125 2.5-2.5v-17.5c0-1.375-1.124-2.5-2.5-2.5zM15.995 13.151c-0.296 0.463-0.454 0.265-0.365 0 0.235-0.698 0.226-2.809-1.74-3.118v6.516c0 1.215-0.7 1.977-2.188 2.455-1.445 0.462-3.063-0.019-3.441-1.057-0.377-1.037 0.473-2.3 1.899-2.82 0.796-0.289 1.604-0.286 2.226-0.045v-9.193h1.504c0 1.579 4.226 3.961 2.105 7.261z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M16.875 3.75c-1.243 0-2.25 0.382-2.25 1.625v14.25c0 1.243 1.007 1.625 2.25 1.625s2.25-0.382 2.25-1.625v-14.25c0-1.243-1.007-1.625-2.25-1.625zM8.125 3.75c-1.243 0-2.25 0.382-2.25 1.625v14.25c0 1.243 1.007 1.625 2.25 1.625s2.25-0.382 2.25-1.625v-14.25c0-1.243-1.007-1.625-2.25-1.625z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M20.873 11.859l-10.701-6.63c-0.78-0.511-1.418-0.134-1.418 0.838v12.87c0 0.971 0.639 1.348 1.418 0.836l10.702-6.63c0 0 0.38-0.268 0.38-0.643-0.001-0.374-0.381-0.641-0.381-0.641z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M18.866 8.368h0.884v2.516l5.25-4.5-5.25-4.5v2.984h-0.884c-4.616 0-7.22 3.421-9.518 6.44-2.066 2.715-3.85 5.060-6.732 5.060h-2.616v3.501h2.616c4.618 0 7.22-3.423 9.519-6.441 2.065-2.716 3.849-5.060 6.731-5.060zM6.764 10.573c0.196-0.255 0.395-0.515 0.596-0.779 0.489-0.643 1.005-1.32 1.564-1.995-1.651-1.543-3.644-2.681-6.307-2.681h-2.616v3.5h2.616c1.659 0 2.954 0.779 4.148 1.955zM19.75 16.616h-0.884c-1.759 0-3.107-0.874-4.363-2.173-0.126 0.164-0.251 0.33-0.379 0.498-0.551 0.723-1.145 1.501-1.799 2.27 1.696 1.655 3.751 2.905 6.54 2.905h0.884v3l5.25-4.5-5.25-4.502v2.502z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M23.709 16.798c0 1.101-0.901 2-2.001 2h-19.5c-1.1 0-1.229-0.461-0.285-1.027l20.070-11.318c0.944-0.566 1.716-0.129 1.716 0.971v9.374z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M20.002 7.75h-13.5v-2.75l-5.25 4.5 5.25 4.5v-2.75h12.5v5.25h-16.25v3.5h17.25c1.38 0 2.5-1.12 2.5-2.5v-7.25c0-1.381-1.121-2.5-2.5-2.5z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M5 21.25c0 1.38 1.12 2.5 2.5 2.5 1.381 0 2.5-1.12 2.5-2.5s-1.119-2.5-2.5-2.5c-1.38 0-2.5 1.12-2.5 2.5zM17.5 21.25c0 1.38 1.12 2.5 2.5 2.5 1.381 0 2.5-1.12 2.5-2.5s-1.119-2.5-2.5-2.5c-1.38 0-2.5 1.12-2.5 2.5zM9.434 15.34l13.836-3.952c0.264-0.076 0.48-0.363 0.48-0.638v-6.875h-17.625v-2.125c0-0.275-0.224-0.5-0.5-0.5h-3.875c-0.275 0-0.5 0.225-0.5 0.5v2h2.429l2.46 11.321 0.238 1.179v1.875c0 0.275 0.225 0.5 0.499 0.5h16.376c0.274 0 0.499-0.225 0.499-0.5v-1.875h-14.060c-1.436 0-1.466-0.562-0.256-0.91z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M9.356 12.5c0 0.323 0.327 0.552 0.327 0.552l9.094 5.711c0.674 0.441 1.223 0.116 1.223-0.719v-11.090c0-0.836-0.549-1.161-1.223-0.72l-9.094 5.713c0 0.001-0.327 0.23-0.327 0.552zM5 6.696v11.607c0 1.11 0.765 1.45 1.875 1.45s1.875-0.34 1.875-1.449v-11.609c0-1.111-0.765-1.451-1.875-1.451s-1.875 0.341-1.875 1.451z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M15.316 11.948l-9.094-5.713c-0.673-0.44-1.223-0.116-1.223 0.721v11.089c0 0.837 0.55 1.16 1.223 0.72l9.094-5.711c0 0 0.329-0.231 0.329-0.552s-0.329-0.554-0.329-0.554zM18.125 5.245c-1.11 0-1.875 0.341-1.875 1.451v11.607c0 1.111 0.765 1.452 1.875 1.452s1.875-0.341 1.875-1.452v-11.607c0-1.11-0.765-1.451-1.875-1.451z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M6.25 16.25h-1.25c-0.69 0-1.25 0.559-1.25 1.25s0.56 1.25 1.25 1.25h1.25c0.69 0 1.25-0.559 1.25-1.25s-0.56-1.25-1.25-1.25zM6.25 11.25h-1.25c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h1.25c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25zM6.25 6.25h-1.25c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h1.25c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25zM11.25 8.75h8.75c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25h-8.75c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25zM20 11.25h-8.75c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h8.75c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25zM20 16.25h-8.75c-0.69 0-1.25 0.559-1.25 1.25s0.56 1.25 1.25 1.25h8.75c0.691 0 1.25-0.559 1.25-1.25s-0.559-1.25-1.25-1.25z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M20 11.25h-15c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25h15c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25zM5 8.75h15c0.691 0 1.25-0.56 1.25-1.25s-0.559-1.25-1.25-1.25h-15c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25zM20 16.25h-15c-0.69 0-1.25 0.559-1.25 1.25s0.56 1.25 1.25 1.25h15c0.691 0 1.25-0.559 1.25-1.25s-0.559-1.25-1.25-1.25z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M21.25 7.75h-4.75v3.5h3.75v5.25h-15.5v-5.25h5.25v2.75l5-4.5-5-4.5v2.75h-6.25c-1.38 0-2.5 1.119-2.5 2.5v7.25c0 1.38 1.12 2.5 2.5 2.5h17.5c1.381 0 2.5-1.12 2.5-2.5v-7.25c0-1.381-1.119-2.5-2.5-2.5z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M18.75 1.25h-12.5c-1.375 0-2.5 1.125-2.5 2.5v17.5c0 1.375 1.125 2.5 2.5 2.5h12.5c1.376 0 2.5-1.125 2.5-2.5v-17.5c0-1.375-1.124-2.5-2.5-2.5zM15.995 13.151c-0.296 0.463-0.454 0.265-0.365 0 0.235-0.698 0.226-2.809-1.74-3.118v6.516c0 1.215-0.7 1.977-2.188 2.455-1.445 0.462-3.063-0.019-3.441-1.057-0.377-1.037 0.473-2.3 1.899-2.82 0.796-0.289 1.604-0.286 2.226-0.045v-9.193h1.504c0 1.579 4.226 3.961 2.105 7.261z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M16.875 3.75c-1.243 0-2.25 0.382-2.25 1.625v14.25c0 1.243 1.007 1.625 2.25 1.625s2.25-0.382 2.25-1.625v-14.25c0-1.243-1.007-1.625-2.25-1.625zM8.125 3.75c-1.243 0-2.25 0.382-2.25 1.625v14.25c0 1.243 1.007 1.625 2.25 1.625s2.25-0.382 2.25-1.625v-14.25c0-1.243-1.007-1.625-2.25-1.625z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M20.873 11.859l-10.701-6.63c-0.78-0.511-1.418-0.134-1.418 0.838v12.87c0 0.971 0.639 1.348 1.418 0.836l10.702-6.63c0 0 0.38-0.268 0.38-0.643-0.001-0.374-0.381-0.641-0.381-0.641z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M18.866 8.368h0.884v2.516l5.25-4.5-5.25-4.5v2.984h-0.884c-4.616 0-7.22 3.421-9.518 6.44-2.066 2.715-3.85 5.060-6.732 5.060h-2.616v3.501h2.616c4.618 0 7.22-3.423 9.519-6.441 2.065-2.716 3.849-5.060 6.731-5.060zM6.764 10.573c0.196-0.255 0.395-0.515 0.596-0.779 0.489-0.643 1.005-1.32 1.564-1.995-1.651-1.543-3.644-2.681-6.307-2.681h-2.616v3.5h2.616c1.659 0 2.954 0.779 4.148 1.955zM19.75 16.616h-0.884c-1.759 0-3.107-0.874-4.363-2.173-0.126 0.164-0.251 0.33-0.379 0.498-0.551 0.723-1.145 1.501-1.799 2.27 1.696 1.655 3.751 2.905 6.54 2.905h0.884v3l5.25-4.5-5.25-4.502v2.502z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M23.709 16.798c0 1.101-0.901 2-2.001 2h-19.5c-1.1 0-1.229-0.461-0.285-1.027l20.070-11.318c0.944-0.566 1.716-0.129 1.716 0.971v9.374z" fill="#ffffff" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M25 12.5c-0.031-1.632-0.385-3.26-1.039-4.748-0.652-1.489-1.598-2.838-2.76-3.953-1.161-1.116-2.538-1.998-4.028-2.579-1.489-0.584-3.091-0.863-4.673-0.829-1.582 0.031-3.158 0.375-4.598 1.010-1.442 0.632-2.748 1.55-3.827 2.675-1.080 1.125-1.933 2.459-2.495 3.901-0.564 1.442-0.833 2.991-0.799 4.523 0.031 1.532 0.365 3.055 0.98 4.448 0.612 1.394 1.501 2.657 2.591 3.7 1.089 1.044 2.38 1.868 3.775 2.41 1.394 0.545 2.892 0.803 4.374 0.77 1.482-0.031 2.953-0.355 4.299-0.95 1.346-0.593 2.566-1.452 3.573-2.506 1.008-1.053 1.803-2.301 2.326-3.648 0.318-0.817 0.534-1.67 0.648-2.534 0.030 0.002 0.061 0.003 0.092 0.003 0.863 0 1.563-0.7 1.563-1.563 0-0.044-0.002-0.087-0.006-0.13h0.006zM22.517 16.649c-0.573 1.299-1.403 2.476-2.421 3.447-1.017 0.972-2.222 1.738-3.522 2.241-1.3 0.505-2.693 0.743-4.074 0.71-1.382-0.032-2.749-0.335-4-0.89-1.251-0.553-2.385-1.355-3.32-2.337-0.936-0.981-1.673-2.143-2.156-3.395-0.485-1.252-0.714-2.593-0.68-3.925 0.032-1.332 0.325-2.647 0.861-3.85 0.534-1.204 1.306-2.294 2.252-3.194 0.946-0.9 2.064-1.608 3.268-2.072 1.205-0.465 2.494-0.684 3.775-0.65 1.282 0.032 2.545 0.315 3.7 0.831 1.156 0.514 2.204 1.257 3.067 2.168 0.864 0.91 1.543 1.985 1.987 3.142 0.446 1.157 0.654 2.394 0.621 3.625h0.006c-0.004 0.043-0.006 0.086-0.006 0.13 0 0.806 0.61 1.469 1.394 1.553-0.152 0.85-0.404 1.68-0.751 2.466z" fill="#000000" />
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25" height="25" viewBox="0 0 25 25">
<g>
</g>
<path d="M25 12.5c-0.031-1.632-0.385-3.26-1.039-4.748-0.652-1.489-1.598-2.838-2.76-3.953-1.161-1.116-2.538-1.998-4.028-2.579-1.489-0.584-3.091-0.863-4.673-0.829-1.582 0.031-3.158 0.375-4.598 1.010-1.442 0.632-2.748 1.55-3.827 2.675-1.080 1.125-1.933 2.459-2.495 3.901-0.564 1.442-0.833 2.991-0.799 4.523 0.031 1.532 0.365 3.055 0.98 4.448 0.612 1.394 1.501 2.657 2.591 3.7 1.089 1.044 2.38 1.868 3.775 2.41 1.394 0.545 2.892 0.803 4.374 0.77 1.482-0.031 2.953-0.355 4.299-0.95 1.346-0.593 2.566-1.452 3.573-2.506 1.008-1.053 1.803-2.301 2.326-3.648 0.318-0.817 0.534-1.67 0.648-2.534 0.030 0.002 0.061 0.003 0.092 0.003 0.863 0 1.563-0.7 1.563-1.563 0-0.044-0.002-0.087-0.006-0.13h0.006zM22.517 16.649c-0.573 1.299-1.403 2.476-2.421 3.447-1.017 0.972-2.222 1.738-3.522 2.241-1.3 0.505-2.693 0.743-4.074 0.71-1.382-0.032-2.749-0.335-4-0.89-1.251-0.553-2.385-1.355-3.32-2.337-0.936-0.981-1.673-2.143-2.156-3.395-0.485-1.252-0.714-2.593-0.68-3.925 0.032-1.332 0.325-2.647 0.861-3.85 0.534-1.204 1.306-2.294 2.252-3.194 0.946-0.9 2.064-1.608 3.268-2.072 1.205-0.465 2.494-0.684 3.775-0.65 1.282 0.032 2.545 0.315 3.7 0.831 1.156 0.514 2.204 1.257 3.067 2.168 0.864 0.91 1.543 1.985 1.987 3.142 0.446 1.157 0.654 2.394 0.621 3.625h0.006c-0.004 0.043-0.006 0.086-0.006 0.13 0 0.806 0.61 1.469 1.394 1.553-0.152 0.85-0.404 1.68-0.751 2.466z" fill="#ffffff" />
</svg>
Patterns from subtlepatterns.com.
"If you need more, that's where to get 'em."
\ No newline at end of file
/**
* SoundManager 2: "Bar UI" player - CSS
* Copyright (c) 2014, Scott Schiller. All rights reserved.
* http://www.schillmania.com/projects/soundmanager2/
* Code provided under BSD license.
* http://schillmania.com/projects/soundmanager2/license.txt
*/
.sm2-bar-ui {
position: relative;
display: inline-block;
width: 100%;
font-family: helvetica, arial, verdana, sans-serif;
font-weight: normal;
/* prevent background border bleed */
-webkit-background-clip: padding-box;
background-clip: padding-box;
/* because indeed, fonts do look pretty "fat" otherwise in this case. */
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
/* general font niceness? */
font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-width: 20em;
max-width: 30em;
/* take out overflow if you want an absolutely-positioned playlist dropdown. */
border-radius: 2px;
overflow: hidden;
/* just for fun (animate normal / full-width) */
transition: max-width 0.2s ease-in-out;
}
.sm2-bar-ui .sm2-playlist li {
text-align: center;
margin-top: -2px;
font-size: 95%;
line-height: 1em;
}
.sm2-bar-ui.compact {
min-width: 1em;
max-width: 15em;
}
.sm2-bar-ui ul {
line-height: 1em;
}
/* want things left-aligned? */
.sm2-bar-ui.left .sm2-playlist li {
text-align: left;
}
.sm2-bar-ui .sm2-playlist li .load-error {
cursor: help;
}
.sm2-bar-ui.full-width {
max-width: 100%;
z-index: 5;
}
.sm2-bar-ui.fixed {
position: fixed;
top: auto;
bottom: 0px;
left: 0px;
border-radius: 0px;
/* so the absolutely-positioned playlist can show... */
overflow: visible;
/* and this should probably have a high z-index. tweak as needed. */
z-index: 999;
}
.sm2-bar-ui.fixed .bd,
.sm2-bar-ui.bottom .bd {
/* display: table; */
border-radius: 0px;
border-bottom: none;
}
.sm2-bar-ui.bottom {
/* absolute bottom-aligned UI */
top: auto;
bottom: 0px;
left: 0px;
border-radius: 0px;
/* so the absolutely-positioned playlist can show... */
overflow: visible;
}
.sm2-bar-ui.playlist-open .bd {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
border-bottom-color: transparent;
}
.sm2-bar-ui .bd,
.sm2-bar-ui .sm2-extra-controls {
position: relative;
background-color: #2288cc;
/*
transition: background 0.2s ease-in-out;
*/
}
.sm2-bar-ui .sm2-inline-gradient {
/* gradient */
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background-image: linear-gradient(to bottom, rgba(255,255,255,0.125) 5%, rgba(255,255,255,0.125) 45%, rgba(255,255,255,0.15) 50%, rgba(0,0,0,0.1) 51%, rgba(0,0,0,0.1) 95%); /* W3C */
}
.sm2-bar-ui.flat .sm2-inline-gradient {
background-image: none;
}
.sm2-bar-ui.flat .sm2-box-shadow {
display: none;
box-shadow: none;
}
.sm2-bar-ui.no-volume .sm2-volume {
/* mobile devices (android + iOS) ignore attempts to set volume. */
display: none;
}
.sm2-bar-ui.textured .sm2-inline-texture {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
/* for example */
/* background-image: url(../images/wood_pattern_dark.png); */
/* additional opacity effects can be applied here. */
opacity: 0.75;
}
.sm2-bar-ui.textured.dark-text .sm2-inline-texture {
/* dark text + textured case: use light wood background (for example.) */
/* background-image: url(../images/patterns/wood_pattern.png); */
}
.sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper {
/* dark text + textured case: ditch 10% dark on playlist body. */
background-color: transparent;
}
.sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper ul li:hover a,
.sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper ul li.selected a {
/* dark + textured case: dark highlights */
background-color: rgba(0,0,0,0.1);
background-image: url(../images/black-10.png);
/* modern browsers don't neeed the image */
background-image: none, none;
}
.sm2-bar-ui .bd {
display: table;
border-bottom: none;
}
.sm2-bar-ui .sm2-playlist-wrapper {
background-color: rgba(0,0,0,0.1);
}
.sm2-bar-ui .sm2-extra-controls .bd {
background-color: rgba(0,0,0,0.2);
}
.sm2-bar-ui.textured .sm2-extra-controls .bd {
/* lighten extra color overlays */
background-color: rgba(0,0,0,0.05);
}
.sm2-bar-ui .sm2-extra-controls {
background-color: transparent;
border: none;
}
.sm2-bar-ui .sm2-extra-controls .bd {
/* override full-width table behaviour */
display: block;
border: none;
}
.sm2-bar-ui .sm2-extra-controls .sm2-inline-element {
display: inline-block;
}
.sm2-bar-ui,
.sm2-bar-ui .bd a {
color: #fff;
}
.sm2-bar-ui.dark-text,
.sm2-bar-ui.dark-text .bd a {
color: #000;
}
.sm2-bar-ui.dark-text .sm2-inline-button {
/* Warning/disclaimer: SVG might be fuzzy when inverted on Chrome, losing resolution on hi-DPI displays. */
-webkit-filter: invert(1);
/* SVG-based invert filter for Firefox */
filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'invert\'><feColorMatrix in='SourceGraphic' type='matrix' values='-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0'/></filter></svg>#invert");
/* IE 8 inverse filter, may only match pure black/white */
/* filter: xray; */
/* pending W3 standard */
filter: invert(1);
/* not you, IE < 10. */
filter: none\9;
}
.sm2-bar-ui .bd a {
text-decoration: none;
}
.sm2-bar-ui .bd .sm2-button-element:hover {
background-color: rgba(0,0,0,0.1);
background-image: url(../images/black-10.png);
background-image: none, none;
}
.sm2-bar-ui .bd .sm2-button-element:active {
background-color: rgba(0,0,0,0.25);
background-image: url(../images/black-25.png);
background-image: none, none;
}
.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active .sm2-inline-button,
.sm2-bar-ui .bd .active .sm2-inline-button/*,
.sm2-bar-ui.playlist-open .sm2-menu a */{
-ms-transform: scale(0.9);
-webkit-transform: scale(0.9);
-webkit-transform-origin: 50% 50%;
/* firefox doesn't scale quite right. */
transform: scale(0.9);
transform-origin: 50% 50%;
/* firefox doesn't scale quite right. */
-moz-transform: none;
}
.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:hover,
.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active,
.sm2-bar-ui .bd .active {
background-color: rgba(0,0,0,0.1);
background-image: url(../images/black-10.png);
background-image: none, none;
}
.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active {
/* box shadow is excessive on smaller elements. */
box-shadow: none;
}
.sm2-bar-ui {
/* base font size */
font-size: 15px;
text-shadow: none;
}
.sm2-bar-ui .sm2-inline-element {
position: relative;
display: inline-block;
vertical-align: middle;
padding: 0px;
overflow: hidden;
}
.sm2-bar-ui .sm2-inline-element,
.sm2-bar-ui .sm2-button-element .sm2-button-bd {
position: relative;
/**
* .sm2-button-bd exists because of a Firefox bug from 2000
* re: nested relative / absolute elements inside table cells.
* https://bugzilla.mozilla.org/show_bug.cgi?id=63895
*/
}
.sm2-bar-ui .sm2-inline-element,
.sm2-bar-ui .sm2-button-element .sm2-button-bd {
/**
* if you play with UI width/height, these are the important ones.
* NOTE: match these values if you want square UI buttons.
*/
min-width: 2.8em;
min-height: 2.8em;
}
.sm2-bar-ui .sm2-inline-button {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.sm2-bar-ui .sm2-extra-controls .bd {
/* don't double-layer. */
background-image: none;
background-color: rgba(0,0,0,0.15);
}
.sm2-bar-ui .sm2-extra-controls .sm2-inline-element {
width: 25px; /* bare minimum */
min-height: 1.75em;
min-width: 2.5em;
}
.sm2-bar-ui .sm2-inline-status {
line-height: 100%;
/* how much to allow before truncating song artist / title with ellipsis */
display: inline-block;
min-width: 200px;
max-width: 20em;
/* a little more spacing */
padding-left: 0.75em;
padding-right: 0.75em;
}
.sm2-bar-ui .sm2-inline-element {
/* extra-small em scales up nicely, vs. 1px which gets fat */
border-right: 0.075em dotted #666; /* legacy */
border-right: 0.075em solid rgba(0,0,0,0.1);
}
.sm2-bar-ui .sm2-inline-element.noborder {
border-right: none;
}
.sm2-bar-ui .sm2-inline-element.compact {
min-width: 2em;
padding: 0px 0.25em;
}
.sm2-bar-ui .sm2-inline-element:first-of-type {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
overflow: hidden;
}
.sm2-bar-ui .sm2-inline-element:last-of-type {
border-right: none;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.sm2-bar-ui .sm2-inline-status a:hover {
background-color: transparent;
text-decoration: underline;
}
.sm2-inline-time,
.sm2-inline-duration {
display: table-cell;
width: 1%;
font-size: 75%;
line-height: 0.9em;
min-width: 2em; /* if you have sounds > 10:00 in length, make this bigger. */
vertical-align: middle;
}
.sm2-bar-ui .sm2-playlist {
position: relative;
height: 1.45em;
}
.sm2-bar-ui .sm2-playlist-target {
/* initial render / empty case */
position: relative;
min-height: 1em;
}
.sm2-bar-ui .sm2-playlist ul {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
list-style-type: none;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.sm2-bar-ui p,
.sm2-bar-ui .sm2-playlist ul,
.sm2-bar-ui .sm2-playlist ul li {
margin: 0px;
padding: 0px;
}
.sm2-bar-ui .sm2-playlist ul li {
position: relative;
}
.sm2-bar-ui .sm2-playlist ul li,
.sm2-bar-ui .sm2-playlist ul li a {
position: relative;
display: block;
/* prevent clipping of characters like "g" */
height: 1.5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.sm2-row {
position: relative;
display: table-row;
}
.sm2-progress-bd {
/* spacing between progress track/ball and time (position) */
padding: 0px 0.8em;
}
.sm2-progress .sm2-progress-track,
.sm2-progress .sm2-progress-ball,
.sm2-progress .sm2-progress-bar {
position: relative;
width: 100%;
height: 0.65em;
border-radius: 0.65em;
}
.sm2-progress .sm2-progress-bar {
/* element which follows the progres "ball" as it moves */
position: absolute;
left: 0px;
top: 0px;
width: 0px;
background-color: rgba(0,0,0,0.33);
background-image: url(../images/black-33.png);
background-image: none, none;
}
.volume-shade,
.playing .sm2-progress .sm2-progress-track,
.paused .sm2-progress .sm2-progress-track {
cursor: pointer;
}
.playing .sm2-progress .sm2-progress-ball {
cursor: -moz-grab;
cursor: -webkit-grab;
cursor: grab;
}
.sm2-progress .sm2-progress-ball {
position: absolute;
top: 0px;
left: 0px;
width: 1em;
height: 1em;
margin: -0.2em 0px 0px -0.5em;
width: 14px;
height: 14px;
margin: -2px 0px 0px -7px;
width: 0.9333em;
height: 0.9333em;
margin: -0.175em 0px 0px -0.466em;
background-color: #fff;
padding: 0px;
/*
z-index: 1;
*/
transition: transform 0.15s ease-in-out;
}
/*
.sm2-bar-ui.dark-text .sm2-progress .sm2-progress-ball {
background-color: #000;
}
*/
.sm2-progress .sm2-progress-track {
background-color: rgba(0,0,0,0.4);
background-image: url(../images/black-33.png); /* legacy */
background-image: none, none; /* modern browsers */
}
/* scrollbar rules have to be separate, browsers not supporting this syntax will skip them when combined. */
.sm2-playlist-wrapper ul::-webkit-scrollbar-track {
background-color: rgba(0,0,0,0.4);
}
.playing.grabbing .sm2-progress .sm2-progress-track,
.playing.grabbing .sm2-progress .sm2-progress-ball {
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
cursor: grabbing;
}
.sm2-bar-ui.grabbing .sm2-progress .sm2-progress-ball {
-webkit-transform: scale(1.15);
transform: scale(1.15);
}
.sm2-inline-button {
background-position: 50% 50%;
background-repeat: no-repeat;
/* hide inner text */
line-height: 10em;
/**
* image-rendering seems to apply mostly to Firefox in this case. Use with caution.
* https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#Browser_compatibility
*/
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
-ms-interpolation-mode: nearest-neighbor;
-ms-interpolation-mode: bicubic;
}
.sm2-icon-play-pause,
.sm2-icon-play-pause:hover,
.paused .sm2-icon-play-pause:hover {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/play.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/play.svg);
background-size: 67.5%;
background-position: 40% 53%;
}
.playing .sm2-icon-play-pause {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/pause.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/pause.svg);
background-size: 57.6%;
background-position: 50% 53%;
}
.sm2-volume-control {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/volume.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/volume.svg);
}
.sm2-volume-control,
.sm2-volume-shade {
background-position: 42% 50%;
background-size: 56%;
}
.volume-shade {
filter: alpha(opacity=33); /* <= IE 8 */
opacity: 0.33;
/* -webkit-filter: invert(1);*/
background-image: url(../images/icomoon/entypo-25px-000000/PNG/volume.png);
background-image: none, url(../images/icomoon/entypo-25px-000000/SVG/volume.svg);
}
.sm2-icon-menu {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/list2.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/list2.svg);
background-size: 58%;
background-position: 54% 51%;
}
.sm2-icon-previous {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/first.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/first.svg);
}
.sm2-icon-next {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/last.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/last.svg);
}
.sm2-icon-previous,
.sm2-icon-next {
background-size: 49.5%;
background-position: 50% 50%;
}
.sm2-extra-controls .sm2-icon-previous,
.sm2-extra-controls .sm2-icon-next {
backgound-size: 53%;
}
.sm2-icon-shuffle {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/shuffle.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/shuffle.svg);
background-size: 45%;
background-position: 50% 50%;
}
.sm2-icon-repeat {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/loop.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/loop.svg);
background-position: 50% 43%;
background-size: 54%;
}
.sm2-extra-controls .sm2-icon-repeat {
background-position: 50% 45%;
}
.sm2-playlist-wrapper ul li .sm2-row {
display: table;
width: 100%;
}
.sm2-playlist-wrapper ul li .sm2-col {
display: table-cell;
vertical-align: top;
/* by default, collapse. */
width: 0%;
}
.sm2-playlist-wrapper ul li .sm2-col.sm2-wide {
/* take 100% width. */
width: 100%;
}
.sm2-playlist-wrapper ul li .sm2-icon {
display: inline-block;
overflow: hidden;
width: 2em;
color: transparent !important; /* hide text */
white-space: nowrap; /* don't let text affect height */
padding-left: 0px;
padding-right: 0px;
text-indent: 2em; /* IE 8, mostly */
}
.sm2-playlist-wrapper ul li .sm2-icon,
.sm2-playlist-wrapper ul li:hover .sm2-icon,
.sm2-playlist-wrapper ul li.selected .sm2-icon {
background-size: 55%;
background-position: 50% 50%;
background-repeat: no-repeat;
}
.sm2-playlist-wrapper ul li .sm2-col {
/* sibling table cells get borders. */
border-right: 1px solid rgba(0,0,0,0.075);
}
.sm2-playlist-wrapper ul li.selected .sm2-col {
border-color: rgba(255,255,255,0.075);
}
.sm2-playlist-wrapper ul li .sm2-col:last-of-type {
border-right: none;
}
.sm2-playlist-wrapper ul li .sm2-cart,
.sm2-playlist-wrapper ul li:hover .sm2-cart,
.sm2-playlist-wrapper ul li.selected .sm2-cart {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/cart.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/cart.svg);
/* slight alignment tweak */
background-position: 48% 50%;
}
.sm2-playlist-wrapper ul li .sm2-music,
.sm2-playlist-wrapper ul li:hover .sm2-music,
.sm2-playlist-wrapper ul li.selected .sm2-music {
background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/music.png);
background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/music.svg);
}
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-cart,
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover .sm2-cart,
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-cart {
background-image: url(../images/icomoon/entypo-25px-000000/PNG/cart.png);
background-image: none, url(../images/icomoon/entypo-25px-000000/SVG/cart.svg);
}
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-music,
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover .sm2-music,
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-music {
background-image: url(../images/icomoon/entypo-25px-000000/PNG/music.png);
background-image: none, url(../images/icomoon/entypo-25px-000000/SVG/music.svg);
}
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-col {
border-left-color: rgba(0,0,0,0.15);
}
.sm2-playlist-wrapper ul li .sm2-icon:hover {
background-color: rgba(0,0,0,0.33);
}
.sm2-bar-ui .sm2-playlist-wrapper ul li .sm2-icon:hover {
background-color: rgba(0,0,0,0.45);
}
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-icon:hover {
background-color: rgba(255,255,255,0.25);
border-color: rgba(0,0,0,0.125);
}
.sm2-progress-ball .icon-overlay {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
background: none, url(../images/icomoon/free-25px-000000/SVG/spinner.svg);
background-size: 72%;
background-position: 50%;
background-repeat: no-repeat;
display: none;
}
.playing.buffering .sm2-progress-ball .icon-overlay {
display: block;
-webkit-animation: spin 0.6s linear infinite;
animation: spin 0.6s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.sm2-element ul {
font-size: 95%;
list-style-type: none;
}
.sm2-element ul,
.sm2-element ul li {
margin: 0px;
padding: 0px;
}
.bd.sm2-playlist-drawer {
/* optional: absolute positioning */
/* position: absolute; */
z-index: 3;
border-radius: 0px;
width: 100%;
height: 0px;
border: none;
background-image: none;
display: block;
overflow: hidden;
transition: height 0.2s ease-in-out;
}
.sm2-bar-ui.fixed .bd.sm2-playlist-drawer,
.sm2-bar-ui.bottom .bd.sm2-playlist-drawer {
position: absolute;
}
.sm2-bar-ui.fixed .sm2-playlist-wrapper,
.sm2-bar-ui.bottom .sm2-playlist-wrapper {
padding-bottom: 0px;
}
.sm2-bar-ui.fixed .bd.sm2-playlist-drawer,
.sm2-bar-ui.bottom .bd.sm2-playlist-drawer {
/* show playlist on top */
bottom: 2.8em;
}
.sm2-bar-ui .bd.sm2-playlist-drawer {
opacity: 0.5;
/* redraw fix for Chrome, background color doesn't always draw when playlist drawer open. */
transform: translateZ(0);
}
/* experimental, may not perform well. */
/*
.sm2-bar-ui .bd.sm2-playlist-drawer a {
-webkit-filter: blur(5px);
}
*/
.sm2-bar-ui.playlist-open .bd.sm2-playlist-drawer {
height: auto;
opacity: 1;
}
.sm2-bar-ui.playlist-open .bd.sm2-playlist-drawer a {
-webkit-filter: none; /* blur(0px) was still blurred on retina displays, as of 07/2014 */
}
.sm2-bar-ui.fixed.playlist-open .bd.sm2-playlist-drawer .sm2-playlist-wrapper,
.sm2-bar-ui.bottom.playlist-open .bd.sm2-playlist-drawer .sm2-playlist-wrapper {
/* extra padding when open */
padding-bottom: 0.5em;
box-shadow: none;
}
.sm2-bar-ui .bd.sm2-playlist-drawer {
transition: all 0.2s ease-in-out;
transition-property: transform, height, opacity, background-color, -webkit-filter;
}
.sm2-bar-ui .bd.sm2-playlist-drawer a {
transition: -webkit-filter 0.2s ease-in-out;
}
.sm2-bar-ui .bd.sm2-playlist-drawer .sm2-inline-texture {
/* negative offset for height of top bar, so background is seamless. */
background-position: 0px -2.8em;
}
.sm2-box-shadow {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
box-shadow: inset 0px 1px 6px rgba(0,0,0,0.15);
}
.sm2-playlist-wrapper {
position: relative;
padding: 0.5em 0.5em 0.5em 0.25em;
background-image: none, none;
}
.sm2-playlist-wrapper ul {
max-height: 9.25em;
overflow: auto;
}
.sm2-playlist-wrapper ul li {
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.sm2-playlist-wrapper ul li:nth-child(odd) {
background-color: rgba(255,255,255,0.03);
}
.sm2-playlist-wrapper ul li a {
display: block;
padding: 0.5em 0.25em 0.5em 0.75em;
margin-right: 0px;
font-size: 90%;
vertical-align: middle;
}
.sm2-playlist-wrapper ul li a.sm2-exclude {
display: inline-block;
}
.sm2-playlist-wrapper ul li a.sm2-exclude .label {
font-size: 95%;
line-height: 1em;
margin-left: 0px;
padding: 2px 4px;
}
.sm2-playlist-wrapper ul li:hover a {
background-color: rgba(0,0,0,0.20);
background-image: url(../images/black-20.png);
background-image: none, none;
}
.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover a {
background-color: rgba(255,255,255,0.1);
background-image: url(../images/black-10.png);
background-image: none, none;
}
.sm2-playlist-wrapper ul li.selected a {
background-color: rgba(0,0,0,0.25);
background-image: url(../images/black-20.png);
background-image: none, none;
}
.sm2-bar-ui.dark-text ul li.selected a {
background-color: rgba(255,255,255,0.1);
background-image: url(../images/black-10.png);
background-image: none, none;
}
.sm2-bar-ui .disabled {
filter: alpha(opacity=33); /* <= IE 8 */
opacity: 0.33;
}
.sm2-bar-ui .bd .sm2-button-element.disabled:hover {
background-color: transparent;
}
.sm2-bar-ui .active,
/*.sm2-bar-ui.playlist-open .sm2-menu,*/
.sm2-bar-ui.playlist-open .sm2-menu:hover {
/* depressed / "on" state */
box-shadow: inset 0px 0px 2px rgba(0,0,0,0.1);
background-image: none;
}
.firefox-fix {
/**
* This exists because of a Firefox bug from 2000
* re: nested relative / absolute elements inside table cells.
* https://bugzilla.mozilla.org/show_bug.cgi?id=63895
*/
position: relative;
display: inline-block;
width: 100%;
height: 100%;
}
/* some custom scrollbar trickery, where supported */
.sm2-playlist-wrapper ul::-webkit-scrollbar {
width: 10px;
}
.sm2-playlist-wrapper ul::-webkit-scrollbar-track {
background: rgba(0,0,0,0.33);
border-radius: 10px;
}
.sm2-playlist-wrapper ul::-webkit-scrollbar-thumb {
border-radius: 10px;
background: #fff;
}
.sm2-extra-controls {
font-size: 0px;
text-align: center;
}
.sm2-bar-ui .label {
position: relative;
display: inline-block;
font-size: 0.7em;
margin-left: 0.25em;
vertical-align: top;
background-color: rgba(0,0,0,0.25);
border-radius: 3px;
padding: 0px 3px;
box-sizing: padding-box;
}
.sm2-bar-ui.dark-text .label {
background-color: rgba(0,0,0,0.1);
background-image: url(../images/black-10.png);
background-image: none, none;
}
.sm2-bar-ui .sm2-playlist-drawer .label {
font-size: 0.8em;
padding: 0px 3px;
}
/* --- full width stuff --- */
.sm2-bar-ui .sm2-inline-element {
display: table-cell;
}
.sm2-bar-ui .sm2-inline-element {
/* collapse */
width: 1%;
}
.sm2-bar-ui .sm2-inline-status {
/* full width */
width: 100%;
min-width: 100%;
max-width: 100%;
}
.sm2-bar-ui > .bd {
width: 100%;
}
.sm2-bar-ui .sm2-playlist-drawer {
/* re-hide playlist */
display: block;
overflow: hidden;
}
.sm2-bar-ui {
font-size: 20px;
}
.sm2-bar-ui.compact {
max-width: 90%;
}
.sm2-progress .sm2-progress-ball {
width: .5333em;
height: 1.9333em;
border-radius: 0em;
}
.sm2-progress .sm2-progress-track {
height: 0.15em;
background: white;
}
.sm2-bar-ui .sm2-main-controls,
.sm2-bar-ui .sm2-playlist-drawer {
background-color: transparent;
}
.sm2-bar-ui .sm2-inline-texture {
background: transparent;
}
.rating .glyphicon-star {
color: gray;
}
.rating .glyphicon-star.good {
color: white;
}
body {
overflow: hidden;
background: #272B30;
color: #aaa;
}
#main {
position: absolute;
width: 100%;
height: 100%;
}
#area {
width: 80%;
height: 80%;
margin: 5% auto;
max-width: 1250px;
}
#area iframe {
border: none;
}
#prev {
left: 40px;
}
#next {
right: 40px;
}
.arrow {
position: absolute;
top: 50%;
margin-top: -32px;
font-size: 64px;
color: #E2E2E2;
font-family: arial, sans-serif;
font-weight: bold;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.arrow:hover {
color: #777;
}
.arrow:active {
color: #000;
}
xmp,
pre,
plaintext {
display: block;
font-family: -moz-fixed;
white-space: pre;
margin: 1em 0;
}
#area {
overflow: hidden;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: -moz-fixed;
column-count: 2;
-webkit-columns: 2;
-moz-columns: 2;
column-gap: 20px;
-moz-column-gap: 20px;
-webkit-column-gap: 20px;
position: relative;
}
\ No newline at end of file
(function (window) {
/**
* SoundManager 2: "Bar UI" player
* Copyright (c) 2014, Scott Schiller. All rights reserved.
* http://www.schillmania.com/projects/soundmanager2/
* Code provided under BSD license.
* http://schillmania.com/projects/soundmanager2/license.txt
*/
/* global console, document, navigator, soundManager, window */
'use strict';
var Player,
players = [],
// CSS selector that will get us the top-level DOM node for the player UI.
playerSelector = '.sm2-bar-ui',
playerOptions,
utils;
/**
* The following are player object event callback examples.
* Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc.
* soundObject is provided for whileplaying() etc., but playback control should be done via the player object.
*/
players.on = {
/*
play: function(player, soundObject) {
console.log('playing', player);
},
whileplaying: function(player, soundObject) {
console.log('whileplaying', player, soundObject);
},
finish: function(player, soundObject) {
// each sound
console.log('finish', player);
},
pause: function(player, soundObject) {
console.log('pause', player);
},
error: function(player, soundObject) {
console.log('error', player);
},
end: function(player, soundObject) {
// end of playlist
console.log('end', player);
}
*/
};
playerOptions = {
// useful when multiple players are in use, or other SM2 sounds are active etc.
stopOtherSounds: true,
// CSS class to let the browser load the URL directly e.g., <a href="foo.mp3" class="sm2-exclude">download foo.mp3</a>
excludeClass: 'sm2-exclude'
};
soundManager.setup({
// trade-off: higher UI responsiveness (play/progress bar), but may use more CPU.
html5PollingInterval: 50,
flashVersion: 9
});
soundManager.onready(function () {
var nodes, i, j;
nodes = utils.dom.getAll(playerSelector);
if (nodes && nodes.length) {
for (i = 0, j = nodes.length; i < j; i++) {
players.push(new Player(nodes[i]));
}
}
});
/**
* player bits
*/
Player = function (playerNode) {
var css, dom, extras, playlistController, soundObject, actions, actionData, defaultItem, defaultVolume, firstOpen, exports;
css = {
disabled: 'disabled',
selected: 'selected',
active: 'active',
legacy: 'legacy',
noVolume: 'no-volume',
playlistOpen: 'playlist-open'
};
dom = {
o: null,
playlist: null,
playlistTarget: null,
playlistContainer: null,
time: null,
player: null,
progress: null,
progressTrack: null,
progressBar: null,
duration: null,
volume: null
};
// prepended to tracks when a sound fails to load/play
extras = {
loadFailedCharacter: '<span title="Failed to load/play." class="load-error">✖</span>'
};
function stopOtherSounds() {
if (playerOptions.stopOtherSounds) {
soundManager.stopAll();
}
}
function callback(method, oSound) {
if (method) {
// fire callback, passing current player and sound objects
if (exports.on && exports.on[method]) {
exports.on[method](exports, oSound);
} else if (players.on[method]) {
players.on[method](exports, oSound);
}
}
}
function getTime(msec, useString) {
// convert milliseconds to hh:mm:ss, return as object literal or string
var nSec = Math.floor(msec / 1000),
hh = Math.floor(nSec / 3600),
min = Math.floor(nSec / 60) - Math.floor(hh * 60),
sec = Math.floor(nSec - (hh * 3600) - (min * 60));
// if (min === 0 && sec === 0) return null; // return 0:00 as null
return (useString ? ((hh ? hh + ':' : '') + (hh && min < 10 ? '0' + min : min) + ':' + (sec < 10 ? '0' + sec : sec)) : { min: min, sec: sec });
}
function setTitle(item) {
// given a link, update the "now playing" UI.
// if this is an <li> with an inner link, grab and use the text from that.
var links = item.getElementsByTagName('a');
if (links.length) {
item = links[0];
}
// remove any failed character sequence, also
dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML.replace(extras.loadFailedCharacter, '') + '</li></ul>';
if (dom.playlistTarget.getElementsByTagName('li')[0].scrollWidth > dom.playlistTarget.offsetWidth) {
// this item can use <marquee>, in fact.
dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li><marquee>' + item.innerHTML + '</marquee></li></ul>';
}
}
function makeSound(url) {
var sound = soundManager.createSound({
url: url,
volume: defaultVolume,
whileplaying: function () {
//This sends a bookmark update to calibreweb every 30 seconds.
if (this.progressBuffer == undefined) {
this.progressBuffer = 0;
}
if (this.progressBuffer <= this.position) {
$.ajax(calibre.bookmarkUrl, {
method: "post",
data: { bookmark: this.position }
}).fail(function (xhr, status, error) {
console.error(error);
});
this.progressBuffer = this.progressBuffer + 30000;
}
var progressMaxLeft = 100,
left,
width;
left = Math.min(progressMaxLeft, Math.max(0, (progressMaxLeft * (this.position / this.durationEstimate)))) + '%';
width = Math.min(100, Math.max(0, (100 * (this.position / this.durationEstimate)))) + '%';
if (this.duration) {
dom.progress.style.left = left;
dom.progressBar.style.width = width;
// TODO: only write changes
dom.time.innerHTML = getTime(this.position, true);
}
callback('whileplaying', this);
},
onbufferchange: function (isBuffering) {
if (isBuffering) {
utils.css.add(dom.o, 'buffering');
} else {
utils.css.remove(dom.o, 'buffering');
}
},
onplay: function () {
utils.css.swap(dom.o, 'paused', 'playing');
callback('play', this);
},
onpause: function () {
$.ajax(calibre.bookmarkUrl, {
method: "post",
data: { bookmark: this.position }
}).fail(function (xhr, status, error) {
console.error(error);
});
utils.css.swap(dom.o, 'playing', 'paused');
callback('pause', this);
},
onresume: function () {
utils.css.swap(dom.o, 'paused', 'playing');
},
whileloading: function () {
if (!this.isHTML5) {
dom.duration.innerHTML = getTime(this.durationEstimate, true);
}
},
onload: function (ok) {
sound.setPosition(calibre.bookmark);
if (ok) {
dom.duration.innerHTML = getTime(this.duration, true);
} else if (this._iO && this._iO.onerror) {
this._iO.onerror();
}
},
onerror: function () {
// sound failed to load.
var item, element, html;
item = playlistController.getItem();
if (item) {
// note error, delay 2 seconds and advance?
// playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML + '</li></ul>';
if (extras.loadFailedCharacter) {
dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('<li>', '<li>' + extras.loadFailedCharacter + ' ');
if (playlistController.data.playlist && playlistController.data.playlist[playlistController.data.selectedIndex]) {
element = playlistController.data.playlist[playlistController.data.selectedIndex].getElementsByTagName('a')[0];
html = element.innerHTML;
if (html.indexOf(extras.loadFailedCharacter) === -1) {
element.innerHTML = extras.loadFailedCharacter + ' ' + html;
}
}
}
}
callback('error', this);
// load next, possibly with delay.
if (navigator.userAgent.match(/mobile/i)) {
// mobile will likely block the next play() call if there is a setTimeout() - so don't use one here.
actions.next();
} else {
if (playlistController.data.timer) {
window.clearTimeout(playlistController.data.timer);
}
playlistController.data.timer = window.setTimeout(actions.next, 2000);
}
},
onstop: function () {
$.ajax(calibre.bookmarkUrl, {
method: "post",
data: { bookmark: this.position }
}).fail(function (xhr, status, error) {
console.error(error);
});
utils.css.remove(dom.o, 'playing');
},
onfinish: function () {
$.ajax(calibre.bookmarkUrl, {
method: "post",
data: { bookmark: this.position }
}).fail(function (xhr, status, error) {
console.error(error);
});
var lastIndex, item;
utils.css.remove(dom.o, 'playing');
dom.progress.style.left = '0%';
lastIndex = playlistController.data.selectedIndex;
callback('finish', this);
// next track?
item = playlistController.getNext();
// don't play the same item over and over again, if at end of playlist (excluding single item case.)
if (item && (playlistController.data.selectedIndex !== lastIndex || (playlistController.data.playlist.length === 1 && playlistController.data.loopMode))) {
playlistController.select(item);
setTitle(item);
stopOtherSounds();
// play next
this.play({
url: playlistController.getURL()
});
} else {
// end of playlist case
// explicitly stop?
// this.stop();
callback('end', this);
}
}
});
return sound;
}
function playLink(link) {
// if a link is OK, play it.
if (soundManager.canPlayURL(link.href)) {
// if there's a timer due to failure to play one track, cancel it.
// catches case when user may use previous/next after an error.
if (playlistController.data.timer) {
window.clearTimeout(playlistController.data.timer);
playlistController.data.timer = null;
}
if (!soundObject) {
soundObject = makeSound(link.href);
}
// required to reset pause/play state on iOS so whileplaying() works? odd.
soundObject.stop();
playlistController.select(link.parentNode);
setTitle(link.parentNode);
// reset the UI
// TODO: function that also resets/hides timing info.
dom.progress.style.left = '0px';
dom.progressBar.style.width = '0px';
stopOtherSounds();
soundObject.play({
url: link.href,
position: 0
});
}
}
function PlaylistController() {
var data;
data = {
// list of nodes?
playlist: [],
// NOTE: not implemented yet.
// shuffledIndex: [],
// shuffleMode: false,
// selection
selectedIndex: 0,
loopMode: false,
timer: null
};
function getPlaylist() {
return data.playlist;
}
function getItem(offset) {
var list,
item;
// given the current selection (or an offset), return the current item.
// if currently null, may be end of list case. bail.
if (data.selectedIndex === null) {
return offset;
}
list = getPlaylist();
// use offset if provided, otherwise take default selected.
offset = (offset !== undefined ? offset : data.selectedIndex);
// safety check - limit to between 0 and list length
offset = Math.max(0, Math.min(offset, list.length));
item = list[offset];
return item;
}
function findOffsetFromItem(item) {
// given an <li> item, find it in the playlist array and return the index.
var list,
i,
j,
offset;
offset = -1;
list = getPlaylist();
if (list) {
for (i = 0, j = list.length; i < j; i++) {
if (list[i] === item) {
offset = i;
break;
}
}
}
return offset;
}
function getNext() {
// don't increment if null.
if (data.selectedIndex !== null) {
data.selectedIndex++;
}
if (data.playlist.length > 1) {
if (data.selectedIndex >= data.playlist.length) {
if (data.loopMode) {
// loop to beginning
data.selectedIndex = 0;
} else {
// no change
data.selectedIndex--;
// end playback
// data.selectedIndex = null;
}
}
} else {
data.selectedIndex = null;
}
return getItem();
}
function getPrevious() {
data.selectedIndex--;
if (data.selectedIndex < 0) {
// wrapping around beginning of list? loop or exit.
if (data.loopMode) {
data.selectedIndex = data.playlist.length - 1;
} else {
// undo
data.selectedIndex++;
}
}
return getItem();
}
function resetLastSelected() {
// remove UI highlight(s) on selected items.
var items,
i, j;
items = utils.dom.getAll(dom.playlist, '.' + css.selected);
for (i = 0, j = items.length; i < j; i++) {
utils.css.remove(items[i], css.selected);
}
}
function select(item) {
var offset,
itemTop,
itemBottom,
containerHeight,
scrollTop,
itemPadding,
liElement;
// remove last selected, if any
resetLastSelected();
if (item) {
liElement = utils.dom.ancestor('li', item);
utils.css.add(liElement, css.selected);
itemTop = item.offsetTop;
itemBottom = itemTop + item.offsetHeight;
containerHeight = dom.playlistContainer.offsetHeight;
scrollTop = dom.playlist.scrollTop;
itemPadding = 8;
if (itemBottom > containerHeight + scrollTop) {
// bottom-align
dom.playlist.scrollTop = (itemBottom - containerHeight) + itemPadding;
} else if (itemTop < scrollTop) {
// top-align
dom.playlist.scrollTop = item.offsetTop - itemPadding;
}
}
// update selected offset, too.
offset = findOffsetFromItem(liElement);
data.selectedIndex = offset;
}
function playItemByOffset(offset) {
var item;
offset = (offset || 0);
item = getItem(offset);
if (item) {
playLink(item.getElementsByTagName('a')[0]);
}
}
function getURL() {
// return URL of currently-selected item
var item, url;
item = getItem();
if (item) {
url = item.getElementsByTagName('a')[0].href;
}
return url;
}
function refreshDOM() {
// get / update playlist from DOM
if (!dom.playlist) {
if (window.console && console.warn) {
console.warn('refreshDOM(): playlist node not found?');
}
return;
}
data.playlist = dom.playlist.getElementsByTagName('li');
}
function initDOM() {
dom.playlistTarget = utils.dom.get(dom.o, '.sm2-playlist-target');
dom.playlistContainer = utils.dom.get(dom.o, '.sm2-playlist-drawer');
dom.playlist = utils.dom.get(dom.o, '.sm2-playlist-bd');
}
function initPlaylistController() {
// inherit the default SM2 volume
defaultVolume = soundManager.defaultOptions.volume;
initDOM();
refreshDOM();
// animate playlist open, if HTML classname indicates so.
if (utils.css.has(dom.o, css.playlistOpen)) {
// hackish: run this after API has returned
window.setTimeout(function () {
actions.menu(true);
}, 1);
}
}
initPlaylistController();
return {
data: data,
refresh: refreshDOM,
getNext: getNext,
getPrevious: getPrevious,
getItem: getItem,
getURL: getURL,
playItemByOffset: playItemByOffset,
select: select
};
}
function isRightClick(e) {
// only pay attention to left clicks. old IE differs where there's no e.which, but e.button is 1 on left click.
if (e && ((e.which && e.which === 2) || (e.which === undefined && e.button !== 1))) {
// http://www.quirksmode.org/js/events_properties.html#button
return true;
}
return false;
}
function getActionData(target) {
// DOM measurements for volume slider
if (!target) {
return;
}
actionData.volume.x = utils.position.getOffX(target);
actionData.volume.y = utils.position.getOffY(target);
actionData.volume.width = target.offsetWidth;
actionData.volume.height = target.offsetHeight;
// potentially dangerous: this should, but may not be a percentage-based value.
actionData.volume.backgroundSize = parseInt(utils.style.get(target, 'background-size'), 10);
// IE gives pixels even if background-size specified as % in CSS. Boourns.
if (window.navigator.userAgent.match(/msie|trident/i)) {
actionData.volume.backgroundSize = (actionData.volume.backgroundSize / actionData.volume.width) * 100;
}
}
function handleMouseDown(e) {
var links,
target;
target = e.target || e.srcElement;
if (isRightClick(e)) {
return;
}
// normalize to <a>, if applicable.
if (target.nodeName.toLowerCase() !== 'a') {
links = target.getElementsByTagName('a');
if (links && links.length) {
target = target.getElementsByTagName('a')[0];
}
}
if (utils.css.has(target, 'sm2-volume-control')) {
// drag case for volume
getActionData(target);
utils.events.add(document, 'mousemove', actions.adjustVolume);
utils.events.add(document, 'touchmove', actions.adjustVolume);
utils.events.add(document, 'mouseup', actions.releaseVolume);
utils.events.add(document, 'touchend', actions.releaseVolume);
// and apply right away
actions.adjustVolume(e);
}
}
function handleMouse(e) {
var target, barX, barWidth, x, clientX, newPosition, sound;
target = dom.progressTrack;
barX = utils.position.getOffX(target);
barWidth = target.offsetWidth;
clientX = utils.events.getClientX(e);
x = (clientX - barX);
newPosition = (x / barWidth);
sound = soundObject;
if (sound && sound.duration) {
sound.setPosition(sound.duration * newPosition);
// a little hackish: ensure UI updates immediately with current position, even if audio is buffering and hasn't moved there yet.
if (sound._iO && sound._iO.whileplaying) {
sound._iO.whileplaying.apply(sound);
}
}
if (e.preventDefault) {
e.preventDefault();
}
return false;
}
function releaseMouse(e) {
utils.events.remove(document, 'mousemove', handleMouse);
utils.events.remove(document, 'touchmove', handleMouse);
utils.css.remove(dom.o, 'grabbing');
utils.events.remove(document, 'mouseup', releaseMouse);
utils.events.remove(document, 'touchend', releaseMouse);
utils.events.preventDefault(e);
return false;
}
function handleProgressMouseDown(e) {
if (isRightClick(e)) {
return;
}
utils.css.add(dom.o, 'grabbing');
utils.events.add(document, 'mousemove', handleMouse);
utils.events.add(document, 'touchmove', handleMouse);
utils.events.add(document, 'mouseup', releaseMouse);
utils.events.add(document, 'touchend', releaseMouse);
handleMouse(e);
}
function handleClick(e) {
var evt,
target,
offset,
targetNodeName,
methodName,
href,
handled;
evt = (e || window.event);
target = evt.target || evt.srcElement;
if (target && target.nodeName) {
targetNodeName = target.nodeName.toLowerCase();
if (targetNodeName !== 'a') {
// old IE (IE 8) might return nested elements inside the <a>, eg., <b> etc. Try to find the parent <a>.
if (target.parentNode) {
do {
target = target.parentNode;
targetNodeName = target.nodeName.toLowerCase();
} while (targetNodeName !== 'a' && target.parentNode);
if (!target) {
// something went wrong. bail.
return false;
}
}
}
if (targetNodeName === 'a') {
// yep, it's a link.
href = target.href;
if (soundManager.canPlayURL(href)) {
// not excluded
if (!utils.css.has(target, playerOptions.excludeClass)) {
// find this in the playlist
playLink(target);
handled = true;
}
} else {
// is this one of the action buttons, eg., play/pause, volume, etc.?
offset = target.href.lastIndexOf('#');
if (offset !== -1) {
methodName = target.href.substr(offset + 1);
if (methodName && actions[methodName]) {
handled = true;
actions[methodName](e);
}
}
}
// fall-through case
if (handled) {
// prevent browser fall-through
return utils.events.preventDefault(evt);
}
}
}
return true;
}
function init() {
// init DOM?
if (!playerNode && window.console && console.warn) {
console.warn('init(): No playerNode element?');
}
dom.o = playerNode;
// are we dealing with a crap browser? apply legacy CSS if so.
if (window.navigator.userAgent.match(/msie [678]/i)) {
utils.css.add(dom.o, css.legacy);
}
if (window.navigator.userAgent.match(/mobile/i)) {
// majority of mobile devices don't let HTML5 audio set volume.
utils.css.add(dom.o, css.noVolume);
}
dom.progress = utils.dom.get(dom.o, '.sm2-progress-ball');
dom.progressTrack = utils.dom.get(dom.o, '.sm2-progress-track');
dom.progressBar = utils.dom.get(dom.o, '.sm2-progress-bar');
dom.volume = utils.dom.get(dom.o, 'a.sm2-volume-control');
// measure volume control dimensions
if (dom.volume) {
getActionData(dom.volume);
}
dom.duration = utils.dom.get(dom.o, '.sm2-inline-duration');
dom.time = utils.dom.get(dom.o, '.sm2-inline-time');
playlistController = new PlaylistController();
defaultItem = playlistController.getItem(0);
playlistController.select(defaultItem);
if (defaultItem) {
setTitle(defaultItem);
}
utils.events.add(dom.o, 'mousedown', handleMouseDown);
utils.events.add(dom.o, 'touchstart', handleMouseDown);
utils.events.add(dom.o, 'click', handleClick);
utils.events.add(dom.progressTrack, 'mousedown', handleProgressMouseDown);
utils.events.add(dom.progressTrack, 'touchstart', handleProgressMouseDown);
}
// ---
actionData = {
volume: {
x: 0,
y: 0,
width: 0,
height: 0,
backgroundSize: 0
}
};
actions = {
play: function (offsetOrEvent) {
/**
* This is an overloaded function that takes mouse/touch events or offset-based item indices.
* Remember, "auto-play" will not work on mobile devices unless this function is called immediately from a touch or click event.
* If you have the link but not the offset, you can also pass a fake event object with a target of an <a> inside the playlist - e.g. { target: someMP3Link }
*/
var target,
href,
e;
if (offsetOrEvent !== undefined && !isNaN(offsetOrEvent)) {
// smells like a number.
playlistController.playItemByOffset(offsetOrEvent);
return;
}
// DRY things a bit
e = offsetOrEvent;
if (e && e.target) {
target = e.target || e.srcElement;
href = target.href;
}
// haaaack - if null due to no event, OR '#' due to play/pause link, get first link from playlist
if (!href || href.indexOf('#') !== -1) {
href = dom.playlist.getElementsByTagName('a')[0].href;
}
if (!soundObject) {
soundObject = makeSound(href);
}
// edge case: if the current sound is not playing, stop all others.
if (!soundObject.playState) {
stopOtherSounds();
}
// TODO: if user pauses + unpauses a sound that had an error, try to play next?
soundObject.togglePause();
// special case: clear "play next" timeout, if one exists.
// edge case: user pauses after a song failed to load.
if (soundObject.paused && playlistController.data.timer) {
window.clearTimeout(playlistController.data.timer);
playlistController.data.timer = null;
}
},
pause: function () {
if (soundObject && soundObject.readyState) {
soundObject.pause();
}
},
resume: function () {
if (soundObject && soundObject.readyState) {
soundObject.resume();
}
},
stop: function () {
// just an alias for pause, really.
// don't actually stop because that will mess up some UI state, i.e., dragging the slider.
return actions.pause();
},
next: function (/* e */) {
var item, lastIndex;
// special case: clear "play next" timeout, if one exists.
if (playlistController.data.timer) {
window.clearTimeout(playlistController.data.timer);
playlistController.data.timer = null;
}
lastIndex = playlistController.data.selectedIndex;
item = playlistController.getNext(true);
// don't play the same item again
if (item && playlistController.data.selectedIndex !== lastIndex) {
playLink(item.getElementsByTagName('a')[0]);
}
},
prev: function (/* e */) {
var item, lastIndex;
lastIndex = playlistController.data.selectedIndex;
item = playlistController.getPrevious();
// don't play the same item again
if (item && playlistController.data.selectedIndex !== lastIndex) {
playLink(item.getElementsByTagName('a')[0]);
}
},
shuffle: function (e) {
// NOTE: not implemented yet.
var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.shuffle'));
if (target && !utils.css.has(target, css.disabled)) {
utils.css.toggle(target.parentNode, css.active);
playlistController.data.shuffleMode = !playlistController.data.shuffleMode;
}
},
repeat: function (e) {
var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.repeat'));
if (target && !utils.css.has(target, css.disabled)) {
utils.css.toggle(target.parentNode, css.active);
playlistController.data.loopMode = !playlistController.data.loopMode;
}
},
menu: function (ignoreToggle) {
var isOpen;
isOpen = utils.css.has(dom.o, css.playlistOpen);
// hackish: reset scrollTop in default first open case. odd, but some browsers have a non-zero scroll offset the first time the playlist opens.
if (playlistController && !playlistController.data.selectedIndex && !firstOpen) {
dom.playlist.scrollTop = 0;
firstOpen = true;
}
// sniff out booleans from mouse events, as this is referenced directly by event handlers.
if (typeof ignoreToggle !== 'boolean' || !ignoreToggle) {
if (!isOpen) {
// explicitly set height:0, so the first closed -> open animation runs properly
dom.playlistContainer.style.height = '0px';
}
isOpen = utils.css.toggle(dom.o, css.playlistOpen);
}
// playlist
dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px';
},
adjustVolume: function (e) {
/**
* NOTE: this is the mousemove() event handler version.
* Use setVolume(50), etc., to assign volume directly.
*/
var backgroundMargin,
pixelMargin,
target,
value,
volume;
value = 0;
target = dom.volume;
// safety net
if (e === undefined) {
return false;
}
// normalize between mouse and touch events
var clientX = utils.events.getClientX(e);
if (!e || clientX === undefined) {
// called directly or with a non-mouseEvent object, etc.
// proxy to the proper method.
if (arguments.length && window.console && window.console.warn) {
console.warn('Bar UI: call setVolume(' + e + ') instead of adjustVolume(' + e + ').');
}
return actions.setVolume.apply(this, arguments);
}
// based on getStyle() result
// figure out spacing around background image based on background size, eg. 60% background size.
// 60% wide means 20% margin on each side.
backgroundMargin = (100 - actionData.volume.backgroundSize) / 2;
// relative position of mouse over element
value = Math.max(0, Math.min(1, (clientX - actionData.volume.x) / actionData.volume.width));
target.style.clip = 'rect(0px, ' + (actionData.volume.width * value) + 'px, ' + actionData.volume.height + 'px, ' + (actionData.volume.width * (backgroundMargin / 100)) + 'px)';
// determine logical volume, including background margin
pixelMargin = ((backgroundMargin / 100) * actionData.volume.width);
volume = Math.max(0, Math.min(1, ((clientX - actionData.volume.x) - pixelMargin) / (actionData.volume.width - (pixelMargin * 2)))) * 100;
// set volume
if (soundObject) {
soundObject.setVolume(volume);
}
defaultVolume = volume;
return utils.events.preventDefault(e);
},
releaseVolume: function (/* e */) {
utils.events.remove(document, 'mousemove', actions.adjustVolume);
utils.events.remove(document, 'touchmove', actions.adjustVolume);
utils.events.remove(document, 'mouseup', actions.releaseVolume);
utils.events.remove(document, 'touchend', actions.releaseVolume);
},
setVolume: function (volume) {
// set volume (0-100) and update volume slider UI.
var backgroundSize,
backgroundMargin,
backgroundOffset,
target,
from,
to;
if (volume === undefined || isNaN(volume)) {
return;
}
if (dom.volume) {
target = dom.volume;
// based on getStyle() result
backgroundSize = actionData.volume.backgroundSize;
// figure out spacing around background image based on background size, eg. 60% background size.
// 60% wide means 20% margin on each side.
backgroundMargin = (100 - backgroundSize) / 2;
// margin as pixel value relative to width
backgroundOffset = actionData.volume.width * (backgroundMargin / 100);
from = backgroundOffset;
to = from + ((actionData.volume.width - (backgroundOffset * 2)) * (volume / 100));
target.style.clip = 'rect(0px, ' + to + 'px, ' + actionData.volume.height + 'px, ' + from + 'px)';
}
// apply volume to sound, as applicable
if (soundObject) {
soundObject.setVolume(volume);
}
defaultVolume = volume;
}
};
init();
// TODO: mixin actions -> exports
exports = {
// Per-instance events: window.sm2BarPlayers[0].on = { ... } etc. See global players.on example above for reference.
on: null,
actions: actions,
dom: dom,
playlistController: playlistController
};
return exports;
};
// barebones utilities for logic, CSS, DOM, events etc.
utils = {
array: (function () {
function compare(property) {
var result;
return function (a, b) {
if (a[property] < b[property]) {
result = -1;
} else if (a[property] > b[property]) {
result = 1;
} else {
result = 0;
}
return result;
};
}
function shuffle(array) {
// Fisher-Yates shuffle algo
var i, j, temp;
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
return {
compare: compare,
shuffle: shuffle
};
}()),
css: (function () {
function hasClass(o, cStr) {
return (o.className !== undefined ? new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false);
}
function addClass(o, cStr) {
if (!o || !cStr || hasClass(o, cStr)) {
return; // safety net
}
o.className = (o.className ? o.className + ' ' : '') + cStr;
}
function removeClass(o, cStr) {
if (!o || !cStr || !hasClass(o, cStr)) {
return;
}
o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), '');
}
function swapClass(o, cStr1, cStr2) {
var tmpClass = {
className: o.className
};
removeClass(tmpClass, cStr1);
addClass(tmpClass, cStr2);
o.className = tmpClass.className;
}
function toggleClass(o, cStr) {
var found,
method;
found = hasClass(o, cStr);
method = (found ? removeClass : addClass);
method(o, cStr);
// indicate the new state...
return !found;
}
return {
has: hasClass,
add: addClass,
remove: removeClass,
swap: swapClass,
toggle: toggleClass
};
}()),
dom: (function () {
function getAll(param1, param2) {
var node,
selector,
results;
if (arguments.length === 1) {
// .selector case
node = document.documentElement;
// first param is actually the selector
selector = param1;
} else {
// node, .selector
node = param1;
selector = param2;
}
// sorry, IE 7 users; IE 8+ required.
if (node && node.querySelectorAll) {
results = node.querySelectorAll(selector);
}
return results;
}
function get(/* parentNode, selector */) {
var results = getAll.apply(this, arguments);
// hackish: if an array, return the last item.
if (results && results.length) {
return results[results.length - 1];
}
// handle "not found" case
return results && results.length === 0 ? null : results;
}
function ancestor(nodeName, element, checkCurrent) {
if (!element || !nodeName) {
return element;
}
nodeName = nodeName.toUpperCase();
// return if current node matches.
if (checkCurrent && element && element.nodeName === nodeName) {
return element;
}
while (element && element.nodeName !== nodeName && element.parentNode) {
element = element.parentNode;
}
return (element && element.nodeName === nodeName ? element : null);
}
return {
ancestor: ancestor,
get: get,
getAll: getAll
};
}()),
position: (function () {
function getOffX(o) {
// http://www.xs4all.nl/~ppk/js/findpos.html
var curleft = 0;
if (o.offsetParent) {
while (o.offsetParent) {
curleft += o.offsetLeft;
o = o.offsetParent;
}
} else if (o.x) {
curleft += o.x;
}
return curleft;
}
function getOffY(o) {
// http://www.xs4all.nl/~ppk/js/findpos.html
var curtop = 0;
if (o.offsetParent) {
while (o.offsetParent) {
curtop += o.offsetTop;
o = o.offsetParent;
}
} else if (o.y) {
curtop += o.y;
}
return curtop;
}
return {
getOffX: getOffX,
getOffY: getOffY
};
}()),
style: (function () {
function get(node, styleProp) {
// http://www.quirksmode.org/dom/getstyles.html
var value;
if (node.currentStyle) {
value = node.currentStyle[styleProp];
} else if (window.getComputedStyle) {
value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp);
}
return value;
}
return {
get: get
};
}()),
events: (function () {
var add, remove, preventDefault, getClientX;
add = function (o, evtName, evtHandler) {
// return an object with a convenient detach method.
var eventObject = {
detach: function () {
return remove(o, evtName, evtHandler);
}
};
if (window.addEventListener) {
o.addEventListener(evtName, evtHandler, false);
} else {
o.attachEvent('on' + evtName, evtHandler);
}
return eventObject;
};
remove = (window.removeEventListener !== undefined ? function (o, evtName, evtHandler) {
return o.removeEventListener(evtName, evtHandler, false);
} : function (o, evtName, evtHandler) {
return o.detachEvent('on' + evtName, evtHandler);
});
preventDefault = function (e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
e.cancelBubble = true;
}
return false;
};
getClientX = function (e) {
// normalize between desktop (mouse) and touch (mobile/tablet/?) events.
// note pageX for touch, which normalizes zoom/scroll/pan vs. clientX.
return (e && (e.clientX || (e.touches && e.touches[0] && e.touches[0].pageX)));
};
return {
add: add,
preventDefault: preventDefault,
remove: remove,
getClientX: getClientX
};
}()),
features: (function () {
var getAnimationFrame,
localAnimationFrame,
localFeatures,
prop,
styles,
testDiv,
transform;
testDiv = document.createElement('div');
/**
* hat tip: paul irish
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/
* https://gist.github.com/838785
*/
localAnimationFrame = (window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| null);
// apply to window, avoid "illegal invocation" errors in Chrome
getAnimationFrame = localAnimationFrame ? function () {
return localAnimationFrame.apply(window, arguments);
} : null;
function has(propName) {
// test for feature support
return (testDiv.style[propName] !== undefined ? propName : null);
}
// note local scope.
localFeatures = {
transform: {
ie: has('-ms-transform'),
moz: has('MozTransform'),
opera: has('OTransform'),
webkit: has('webkitTransform'),
w3: has('transform'),
prop: null // the normalized property value
},
rotate: {
has3D: false,
prop: null
},
getAnimationFrame: getAnimationFrame
};
localFeatures.transform.prop = (
localFeatures.transform.w3 ||
localFeatures.transform.moz ||
localFeatures.transform.webkit ||
localFeatures.transform.ie ||
localFeatures.transform.opera
);
function attempt(style) {
try {
testDiv.style[transform] = style;
} catch (e) {
// that *definitely* didn't work.
return false;
}
// if we can read back the style, it should be cool.
return !!testDiv.style[transform];
}
if (localFeatures.transform.prop) {
// try to derive the rotate/3D support.
transform = localFeatures.transform.prop;
styles = {
css_2d: 'rotate(0deg)',
css_3d: 'rotate3d(0,0,0,0deg)'
};
if (attempt(styles.css_3d)) {
localFeatures.rotate.has3D = true;
prop = 'rotate3d';
} else if (attempt(styles.css_2d)) {
prop = 'rotate';
}
localFeatures.rotate.prop = prop;
}
testDiv = null;
return localFeatures;
}())
};
// ---
// expose to global
window.sm2BarPlayers = players;
window.sm2BarPlayerOptions = playerOptions;
window.SM2BarPlayer = Player;
}(window));
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -58,9 +58,10 @@
{% endif %}
{% endif %}
{% if reader_list %}
{% if audioentries|length %}
<div class="btn-group" role="group">
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in browser')}}
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-music"></span> {{_('Listen in browser')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
......@@ -68,8 +69,18 @@
<li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
</ul>
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
{% for format in entry.data %}
{% if format.format|lower in audioentries %}
<li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format.format|lower) }}">{{format.format}}</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
</div>
<h2 id="title">{{entry.title|shortentitle(40)}}</h2>
......
......@@ -72,6 +72,11 @@
&amp;
{% endif %}
{% endfor %}
{% for format in entry.data %}
{% if format.format|lower == 'mp3' %}
<span class="glyphicon glyphicon-music"></span>
{% endif %}
{%endfor%}
</p>
{% if entry.ratings.__len__() > 0 %}
<div class="rating">
......
<!DOCTYPE html>
<html class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ entry.title }}</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes">
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/soundmanager2.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bar-ui.js') }}"></script>
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/bar-ui.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/listen.css') }}" />
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/popup.css') }}"> -->
<script>
"use strict";
</script>
</head>
<body>
<div id="main">
<div class="content">
<h2>{{ entry.title }}</h2>
<div class="cover">
{% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> {% endif %}
</div>
{% if entry.ratings.__len__() > 0 %}
<div class="rating">
<p>
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index
< 5 %} {% for numer in range(5 - loop.index) %} <span class="glyphicon glyphicon-star">
</span>
{% endfor %} {% endif %} {% endfor %}
</p>
</div>
{% endif %}
<h3>{{_('Description:')}}</h3>
{{entry.comments[0].text|safe}}
</div>
<div class="sm2-bar-ui compact flat">
<div class="bd sm2-main-controls">
<div class="sm2-inline-texture"></div>
<div class="sm2-inline-gradient"></div>
<div class="sm2-inline-element sm2-button-element">
<div class="sm2-button-bd">
<a href="#play" class="sm2-inline-button sm2-icon-play-pause">Play / pause</a>
</div>
</div>
<div class="sm2-inline-element sm2-inline-status">
<div class="sm2-playlist">
<div class="sm2-playlist-target">
<!-- playlist <ul> + <li> markup will be injected here -->
<!-- if you want default / non-JS content, you can put that here. -->
<noscript>
<p>JavaScript is required.</p>
</noscript>
</div>
</div>
<div class="sm2-progress">
<div class="sm2-row">
<div class="sm2-inline-time">0:00</div>
<div class="sm2-progress-bd">
<div class="sm2-progress-track">
<div class="sm2-progress-bar"></div>
<div class="sm2-progress-ball">
<div class="icon-overlay"></div>
</div>
</div>
</div>
<div class="sm2-inline-duration">0:00</div>
</div>
</div>
</div>
<div class="sm2-inline-element sm2-button-element sm2-volume">
<div class="sm2-button-bd">
<span class="sm2-inline-button sm2-volume-control volume-shade"></span>
<a href="#volume" class="sm2-inline-button sm2-volume-control">volume</a>
</div>
</div>
</div>
<div class="bd sm2-playlist-drawer sm2-element">
<div class="sm2-inline-texture">
<div class="sm2-box-shadow"></div>
</div>
<!-- playlist content is mirrored here -->
<div class="sm2-playlist-wrapper">
<ul class="sm2-playlist-bd">
<li>
<a href="{{ url_for('serve_book', book_id=mp3file,book_format=audioformat)}}"></a>
</li>
</ul>
</div>
</div>
</div>
<script>
window.calibre = {
filePath: "{{ url_for('static', filename='js/libs/') }}",
cssPath: "{{ url_for('static', filename='css/') }}",
bookUrl: "{{ url_for('static', filename=mp3file) }}/",
bookmarkUrl: "{{ url_for('bookmark', book_id=mp3file, book_format=audioformat.upper()) }}",
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
useBookmarks: "{{ g.user.is_authenticated | tojson }}"
};
</script>
</div>
</body>
</html>
\ No newline at end of file
......@@ -82,9 +82,9 @@
filePath: "{{ url_for('static', filename='js/libs/') }}",
cssPath: "{{ url_for('static', filename='css/') }}",
bookmarkUrl: "{{ url_for('bookmark', book_id=bookid, book_format='EPUB') }}",
bookUrl: "{{ url_for('get_download_link_ext', book_id=bookid, book_format="epub", anyname='file.epub') }}",
bookUrl: "{{ url_for('get_download_link_ext', book_id=bookid, book_format='epub', anyname='file.epub') }}",
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
useBookmarks: {{ g.user.is_authenticated | tojson }}
useBookmarks: "{{ g.user.is_authenticated | tojson }}"
};
</script>
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
......
......@@ -136,8 +136,13 @@ gdrive_watch_callback_token = 'target=calibreweb-watch_files'
py3_gevent_link = None
py3_restart_Typ = False
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2', 'html', 'rtf', 'odt'}
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz'}
'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'}
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'html', 'rtf', 'odt'}
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'}
# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else []))
# Main code
......@@ -152,6 +157,13 @@ mimetypes.add_type('application/x-cbr', '.cbr')
mimetypes.add_type('application/x-cbz', '.cbz')
mimetypes.add_type('application/x-cbt', '.cbt')
mimetypes.add_type('image/vnd.djvu', '.djvu')
mimetypes.add_type('application/mpeg', '.mpeg')
mimetypes.add_type('application/mpeg', '.mp3')
mimetypes.add_type('application/mp4', '.m4a')
mimetypes.add_type('application/mp4', '.m4b')
mimetypes.add_type('application/ogg', '.ogg')
mimetypes.add_type('application/ogg', '.oga')
app = (Flask(__name__))
......@@ -1586,7 +1598,12 @@ def show_book(book_id):
kindle_list = helper.check_send_to_kindle(entries)
reader_list = helper.check_read_formats(entries)
return render_title_template('detail.html', entry=entries, cc=cc, is_xhr=request.is_xhr,
audioentries = []
for media_format in entries.data:
if media_format.format.lower() in EXTENSIONS_AUDIO:
audioentries.append(media_format.format.lower())
return render_title_template('detail.html', entry=entries, audioentries=audioentries, cc=cc, is_xhr=request.is_xhr,
title=entries.title, books_shelfs=book_in_shelfs,
have_read=have_read, kindle_list=kindle_list, reader_list=reader_list, page="book")
else:
......@@ -2035,7 +2052,7 @@ def serve_book(book_id, book_format):
book_format = book_format.split(".")[0]
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper()).first()
app.logger.info(data.name)
app.logger.info('Serving book: %s', data.name)
if config.config_use_google_drive:
headers = Headers()
try:
......@@ -2132,17 +2149,29 @@ def read_book(book_id, book_format):
return redirect(url_for("index"))
# check if book was downloaded before
lbookmark = None
bookmark = None
if current_user.is_authenticated:
lbookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id),
bookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id),
ub.Bookmark.book_id == book_id,
ub.Bookmark.format == book_format.upper())).first()
if book_format.lower() == "epub":
return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=lbookmark)
return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=bookmark)
elif book_format.lower() == "pdf":
return render_title_template('readpdf.html', pdffile=book_id, title=_(u"Read a Book"))
elif book_format.lower() == "txt":
return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book"))
elif book_format.lower() == "mp3":
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(),
title=_(u"Read a Book"), entry=entries, bookmark=bookmark)
elif book_format.lower() == "m4b":
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(),
title=_(u"Read a Book"), entry=entries, bookmark=bookmark)
elif book_format.lower() == "m4a":
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(),
title=_(u"Read a Book"), entry=entries, bookmark=bookmark)
else:
book_dir = os.path.join(config.get_main_dir, "cps", "static", str(book_id))
if not os.path.exists(book_dir):
......@@ -3843,7 +3872,7 @@ def upload():
gdriveutils.updateGdriveCalibreFromLocal()
if error:
flash(error, category="error")
uploadText=(u"File %s" % book.title)
uploadText=_(u"File %(file)s uploaded", file=book.title)
helper.global_WorkerThread.add_upload(current_user.nickname,
"<a href=\"" + url_for('show_book', book_id=book.id) + "\">" + uploadText + "</a>")
......
......@@ -6,6 +6,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png)
## Features
- Bootstrap 3 HTML5 interface
......@@ -27,6 +28,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Ability to hide content based on categories for certain users
- Self update capability
- "Magic Link" login to make it easy to log on eReaders
- Audiobook support (see notes below)
## Quick start
......@@ -213,3 +215,11 @@ Currently supported are 2 options, which are both useful for running multiple in
`"-g path"` allows to specify the location of the google-drive database
`"-c path"` allows to specify the location of SSL certfile, works only in combination with keyfile
`"-k path"` allows to specify the location of SSL keyfile, works only in combination with certfile
## Audiobook support
Calibre-web has "limited" audiobook support. This feature is new and requires some testing. Any modern browser should be able to support the new feature. Testing from mobile devices is needed.
Files with .mp3, .m4a, and .m4b can now be uploaded. These files will appear as a downloaded item in the book details page. If an .mp3 file exists, the site will offer a Listen in Browser option.
While you can load mulitple audiobook formats for a book, the site is configured to work with the entire book in one format. If you have a multi-file collection for your audiobook, convert it to a single file before upload. Otherwise, the system may assume a different book per file!
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment