aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/library/__init__.py0
-rw-r--r--src/library/command.py21
-rw-r--r--src/main.py71
-rw-r--r--src/new/__init__.py0
-rw-r--r--src/new/command.py94
-rw-r--r--src/new/format.py76
-rw-r--r--src/new/reader.py55
-rw-r--r--src/view/__init__.py0
-rw-r--r--src/view/client/book.ts15
-rw-r--r--src/view/client/lib/functions.ts7
-rw-r--r--src/view/client/lib/i18n.ts14
-rw-r--r--src/view/client/lib/rx.ts404
-rw-r--r--src/view/client/lib/search.ts17
-rw-r--r--src/view/client/main.ts46
-rw-r--r--src/view/client/view/books.ts116
-rw-r--r--src/view/client/view/components/modal.ts38
-rw-r--r--src/view/client/view/filters.ts90
-rw-r--r--src/view/command.py17
-rw-r--r--src/view/public/index.html7
-rw-r--r--src/view/public/main.css235
-rw-r--r--src/view/tsconfig.json13
21 files changed, 0 insertions, 1336 deletions
diff --git a/src/library/__init__.py b/src/library/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/src/library/__init__.py
+++ /dev/null
diff --git a/src/library/command.py b/src/library/command.py
deleted file mode 100644
index 6b8577e..0000000
--- a/src/library/command.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import glob
-import json
-import os
-import tomllib
-
-def run(book_library):
- print(get(book_library))
-
-def get(book_library):
- metadatas = []
-
- for path in glob.glob(f'{book_library}/**/metadata.toml', recursive=True):
- with open(path, 'rb') as f:
- directory = os.path.dirname(os.path.realpath(path))
- metadata = tomllib.load(f)
- for p in glob.glob(f'{directory}/cover.*'):
- metadata['cover'] = p
- break
- metadatas.append(metadata)
-
- return json.dumps(metadatas)
diff --git a/src/main.py b/src/main.py
deleted file mode 100644
index 1f07785..0000000
--- a/src/main.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Manage book library.
-#
-# Required dependencies:
-#
-# - python >= 3.11
-# - requests
-# - pillow
-# - ebook-convert CLI (from calibre)
-
-import sys
-import os
-
-import library.command
-import view.command
-import new.command
-
-def print_help(title='Manage book library'):
- print(f"""{title}
-
-- Insert book entry with optional ebook file:
-
- $ python {sys.argv[0]} new [path-to-book]
-
-- Print library metadata as json:
-
- $ python {sys.argv[0]} library
-
-- View books in web page:
-
- $ python {sys.argv[0]} view browser-cmd
-
-Environment variables:
-
- BOOK_LIBRARY: path to book library.""")
-
-def get_book_library():
- path = os.getenv('BOOK_LIBRARY')
- if path is None or not os.path.isdir(path):
- print_help(title='BOOK_LIBRARY environment variable is required.')
- exit(1)
- else:
- return path
-
-def main():
- match sys.argv:
- case [ _, 'new' ]:
- book_library = get_book_library()
- new.command.run(book_library)
- case [ _, 'new', book_source ]:
- if os.path.isfile(book_source):
- book_library = get_book_library()
- new.command.run(book_library, book_source)
- else:
- print_help(title=f'File not found: {book_source}.')
- exit(1)
- case [ _, 'library' ]:
- book_library = get_book_library()
- library.command.run(book_library)
- case [ _, 'view', browser_cmd ]:
- book_library = get_book_library()
- view.command.run(book_library, browser_cmd)
- case [ _, '--help' ]:
- print_help()
- case [ _, '-h' ]:
- print_help()
- case _:
- print_help('Command not found.')
- exit(1)
-
-if __name__ == "__main__":
- main()
diff --git a/src/new/__init__.py b/src/new/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/src/new/__init__.py
+++ /dev/null
diff --git a/src/new/command.py b/src/new/command.py
deleted file mode 100644
index fe706c2..0000000
--- a/src/new/command.py
+++ /dev/null
@@ -1,94 +0,0 @@
-import PIL.Image
-import io
-import os
-import pathlib
-import re
-import requests
-import shutil
-import subprocess
-import sys
-import unicodedata
-import urllib.request
-
-import new.reader as reader
-import new.format as format
-
-def run(book_library, book_source=None):
-
- # Get data
-
- title = reader.required('Title')
- subtitle = reader.optional('Subtitle')
- authors = reader.non_empty_list('Authors')
- author_sort = reader.required('Authors sorting')
- genres = reader.non_empty_list('Genres')
- year = reader.integer('Year')
- lang = reader.choices('Lang', ['fr', 'en', 'de'])
- summary = format.cleanup_text(reader.multi_line('Summary'), lang)
- cover_url = reader.required('Cover url')
- read = reader.choices('Read', ['Read', 'Unread', 'Reading', 'Stopped'])
-
- # Output paths
-
- author_path = format.path_part(author_sort)
- title_path = format.path_part(title)
-
- library_path = '/home/joris/documents/books'
- output_dir = f'{library_path}/{author_path}/{title_path}'
- metadata_path = f'{output_dir}/metadata.toml'
- cover_path = f'{output_dir}/cover.webp'
-
- if not book_source is None:
- ext = format.extension(book_source)
- book_path = f'{output_dir}/book{ext}'
- book_source_dir = os.path.dirname(os.path.realpath(book_source))
- book_source_new = f'{book_source_dir}/{author_path}-{title_path}.mobi'
-
- # Metadata
-
- metadata = f"""title = "{title}"
- subtitle = "{subtitle}"
- authors = {format.format_list(authors)}
- authorsSort = "{author_sort}"
- genres = {format.format_list(genres)}
- date = {year}
- summary = \"\"\"
- {summary}
- \"\"\"
- read = "{read}"
- """
-
- # Ask for confirmation
-
- print(f'About to create:\n\n- {metadata_path}\n- {cover_path}')
- if not book_source is None:
- print(f'- {book_path}')
- print(f'\nAnd moving:\n\n {book_source},\n -> {book_source_new}.')
- print()
-
- reader.confirm('OK?')
-
- # Create files
-
- pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True)
- download_cover(cover_url, cover_path)
- with open(metadata_path, 'w') as f:
- f.write(metadata)
- if not book_path is None:
- shutil.copyfile(book_source, book_path)
- if format.extension(book_source) in ['mobi', 'azw3']:
- os.rename(book_source, book_source_new)
- else:
- subprocess.run(['ebook-convert', book_source, book_source_new])
- os.remove(book_source)
-
-# Download cover as WEBP
-def download_cover(url, path):
- response = requests.get(url, headers={ 'User-Agent': 'python-script' })
- image = PIL.Image.open(io.BytesIO(response.content))
- width, height = image.size
- if width > 300:
- image = image.resize((300, int(300 * height / width)), PIL.Image.LANCZOS)
- image = image.convert('RGB')
- image.save(path, 'WEBP', optimize=True, quality=85)
-
diff --git a/src/new/format.py b/src/new/format.py
deleted file mode 100644
index a712544..0000000
--- a/src/new/format.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import PIL.Image
-import io
-import os
-import pathlib
-import re
-import requests
-import shutil
-import subprocess
-import sys
-import unicodedata
-import urllib.request
-
-def format_list(xs):
- return '[ ' + ', '.join([f'"{x}"' for x in xs]) + ' ]'
-
-def path_part(name):
- simplified = ''.join([alnum_or_space(c) for c in unaccent(name.lower())])
- return '-'.join(simplified.split())
-
-def unaccent(s):
- return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
-
-def alnum_or_space(c):
- if c.isalnum():
- return c
- else:
- return ' '
-
-def extension(path):
- return pathlib.Path(path).suffix
-
-def cleanup_text(s, lang):
- s = re.sub('\'', '’', s)
- s = re.sub('\.\.\.', '…', s)
- s = re.sub('\. \. \.', '…', s)
- s = cleanup_quotes(s, lang)
-
- if lang == 'fr':
- s = re.sub('“', '«', s)
- s = re.sub('”', '»', s)
-
- # Replace space by insecable spaces
- s = re.sub(r' ([:?\!»])', r' \1', s)
- s = re.sub('« ', '« ', s)
-
- # Add missing insecable spaces
- s = re.sub(r'([^ ])([:?\!»])', r'\1 \2', s)
- s = re.sub(r'«([^ ])', r'« \1', s)
-
- elif lang == 'en':
- s = re.sub('«', '“', s)
- s = re.sub('»', '”', s)
-
- return s
-
-def cleanup_quotes(s, lang):
- res = ''
- quoted = False
- for c in s:
- if c == '"':
- if quoted:
- quoted = False
- if lang == 'fr':
- res += '»'
- elif lang == 'en':
- res += '”'
- else:
- quoted = True
- if lang == 'fr':
- res += '«'
- elif lang == 'en':
- res += '“'
- else:
- res += c
- return res
-
diff --git a/src/new/reader.py b/src/new/reader.py
deleted file mode 100644
index eacd70b..0000000
--- a/src/new/reader.py
+++ /dev/null
@@ -1,55 +0,0 @@
-def required(label):
- value = input(f'{label}: ').strip()
- if value:
- print()
- return value
- else:
- return required(label)
-
-def multi_line(label):
- lines = ''
- print(f'{label}, type [end] to finish:\n')
- while True:
- value = input()
- if value.strip() == '[end]':
- break
- elif value.strip():
- lines += f'{value.strip()}\n'
- print()
- return lines.strip()
-
-def optional(label):
- value = input(f'{label} (optional): ').strip()
- print()
- return value
-
-def non_empty_list(label):
- value = input(f'{label} (separated by commas): ')
- values = [x.strip() for x in value.split(',') if x.strip()]
- if len(values) > 0:
- print()
- return values
- else:
- return non_empty_list(label)
-
-def integer(label):
- value = input(f'{label}: ').strip()
- if value.isdigit():
- print()
- return int(value)
- else:
- return integer(label)
-
-def choices(label, xs):
- pp_choices = '/'.join(xs)
- value = input(f'{label} [{pp_choices}] ')
- if value in xs:
- print()
- return value
- else:
- return choices(label, xs)
-
-def confirm(message):
- if choices(message, ['y', 'n']) == 'n':
- print('\nStopping.')
- exit(1)
diff --git a/src/view/__init__.py b/src/view/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/src/view/__init__.py
+++ /dev/null
diff --git a/src/view/client/book.ts b/src/view/client/book.ts
deleted file mode 100644
index 680cc11..0000000
--- a/src/view/client/book.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export interface Book {
- title: string
- subtitle?: string
- authors: Array<string>
- authorsSort: string
- genres: Array<string>
- date: number
- summary?: string
- read: ReadStatus,
- cover: string
-}
-
-export type ReadStatus = 'Read' | 'Unread' | 'Reading' | 'Stopped'
-
-export const readStatuses: Array<ReadStatus> = ['Read', 'Unread', 'Reading', 'Stopped' ]
diff --git a/src/view/client/lib/functions.ts b/src/view/client/lib/functions.ts
deleted file mode 100644
index 21fdad9..0000000
--- a/src/view/client/lib/functions.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export function debounce(func: Function, timeout: number): any {
- let timer: any
- return (...args: any) => {
- clearTimeout(timer)
- timer = setTimeout(() => { func.apply(this, args) }, timeout)
- }
-}
diff --git a/src/view/client/lib/i18n.ts b/src/view/client/lib/i18n.ts
deleted file mode 100644
index 3716367..0000000
--- a/src/view/client/lib/i18n.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export function unit(
- n: number,
- singular: string,
- plural: string,
- f: (n: number, unit: string) => string = format
-): string {
- return n > 1
- ? f(n, plural)
- : f(n, singular)
-}
-
-function format(n: number, unit: string): string {
- return `${n} ${unit}`
-}
diff --git a/src/view/client/lib/rx.ts b/src/view/client/lib/rx.ts
deleted file mode 100644
index bf01b6d..0000000
--- a/src/view/client/lib/rx.ts
+++ /dev/null
@@ -1,404 +0,0 @@
-// [1.0.1] 2023-02-13
-
-// Html
-
-export type Html
- = false
- | undefined
- | string
- | number
- | Tag
- | WithVar<any>
- | Array<Html>
- | Rx<Html>
-
-interface Tag {
- type: 'Tag'
- tagName: string
- attributes: Attributes
- children?: Array<Html>
- onmount?: (element: Element) => void
- onunmount?: (element: Element) => void
-}
-
-interface WithVar<A> {
- type: 'WithVar'
- init: A
- getChildren: (v: Var<A>, update: (f: (value: A) => A) => void) => Html
-}
-
-interface Attributes {
- [key: string]: Rx<AttributeValue> | AttributeValue
-}
-
-type AttributeValue
- = string
- | number
- | boolean
- | ((event: Event) => void)
- | ((element: Element) => void)
-
-function isHtml(x: any): x is Html {
- return (typeof x === 'string'
- || typeof x === 'number'
- || isTag(x)
- || isWithVar(x)
- || isRx(x)
- || Array.isArray(x))
-}
-
-type ValueOrArray<T> = T | Array<ValueOrArray<T>>
-
-export function h(
- tagName: string,
- x?: Attributes | Html,
- ...children: Array<Html>
-): Tag {
- if (x === undefined || x === false) {
- return {
- type: 'Tag',
- tagName,
- attributes: {}
- }
- } else if (isHtml(x)) {
- return {
- type: 'Tag',
- tagName,
- attributes: {},
- children: [x, ...children],
- }
- } else {
- let attributes = x as Attributes
- let onmount, onunmount
- if ('onmount' in attributes) {
- onmount = attributes['onmount'] as (element: Element) => void
- delete attributes['onmount']
- }
- if ('onunmount' in attributes) {
- onunmount = attributes['onunmount'] as (element: Element) => void
- delete attributes['onunmount']
- }
- return {
- type: 'Tag',
- tagName,
- attributes,
- children,
- onmount,
- onunmount
- }
- }
-}
-
-export function withVar<A>(init: A, getChildren: (v: Var<A>, update: (f: (value: A) => A) => void) => Html): WithVar<A> {
- return {
- type: 'WithVar',
- init,
- getChildren
- }
-}
-
-// Rx
-
-export type RxAble<A> = Rx<A> | A
-
-export class Rx<A> {
- map<B>(f: (value: A) => B): Rx<B> {
- return new Map<A, B>(this, f)
- }
-
- flatMap<B>(f: (value: A) => Rx<B>): Rx<B> {
- return new FlatMap<A, B>(this, f)
- }
-}
-
-class Var<A> extends Rx<A> {
- readonly type: 'Var'
- readonly id: string
-
- constructor(id: string) {
- super()
- this.id = id
- this.type = 'Var'
- }
-}
-
-class Map<A, B> extends Rx<B> {
- readonly type: 'Map'
- readonly rx: Rx<A>
- readonly f: (value: A) => B
-
- constructor(rx: Rx<A>, f: (value: A) => B) {
- super()
- this.type = 'Map'
- this.rx = rx
- this.f = f
- }
-}
-
-class FlatMap<A, B> extends Rx<B> {
- readonly type: 'FlatMap'
- readonly rx: Rx<A>
- readonly f: (value: A) => Rx<B>
-
- constructor(rx: Rx<A>, f: (value: A) => Rx<B>) {
- super()
- this.type = 'FlatMap'
- this.rx = rx
- this.f = f
- }
-}
-
-// Mount
-
-export function mount(html: Html): Cancelable {
- const state = new State()
- let appendRes = appendChild(state, document.body, html)
- return appendRes.cancel
-}
-
-interface StateEntry<A> {
- value: A
- subscribers: Array<(value: A) => void>
-}
-
-class State {
- readonly state: {[key: string]: StateEntry<any>}
- varCounter: bigint
-
- constructor() {
- this.state = {}
- this.varCounter = BigInt(0)
- }
-
- register<A>(initValue: A) {
- const v = new Var(this.varCounter.toString())
- this.varCounter += BigInt(1)
- this.state[v.id] = {
- value: initValue,
- subscribers: []
- }
- return v
- }
-
- unregister<A>(v: Var<A>) {
- delete this.state[v.id]
- }
-
- get<A>(v: Var<A>) {
- return this.state[v.id].value
- }
-
- update<A>(v: Var<A>, f: (value: A) => A) {
- const value = f(this.state[v.id].value)
- this.state[v.id].value = value
- this.state[v.id].subscribers.forEach(notify => {
- // Don’t notify if it has been removed from a precedent notifier
- if (this.state[v.id].subscribers.indexOf(notify) !== -1) {
- notify(value)
- }
- })
- }
-
- subscribe<A>(v: Var<A>, notify: (value: A) => void): Cancelable {
- this.state[v.id].subscribers.push(notify)
- return () => this.state[v.id].subscribers = this.state[v.id].subscribers.filter(n => n !== notify)
- }
-}
-
-// Cancelable
-
-type Cancelable = () => void
-
-const voidCancel = () => {}
-
-// Removable
-
-type Removable = () => void
-
-const voidRemove = () => {}
-
-// Rx run
-
-function rxRun<A>(state: State, rx: Rx<A>, effect: (value: A) => void): Cancelable {
- if (isVar(rx)) {
- const cancel = state.subscribe(rx, effect)
- effect(state.get(rx))
- return cancel
- } else if (isMap<A, any>(rx)) {
- return rxRun(state, rx.rx, value => effect(rx.f(value)))
- } else if (isFlatMap(rx)) {
- let cancel1 = voidCancel
- const cancel2 = rxRun(state, rx.rx, (value: A) => {
- cancel1()
- cancel1 = rxRun(state, rx.f(value), effect)
- })
- return () => {
- cancel2()
- cancel1()
- }
- } else {
- throw new Error(`Unrecognized rx: ${rx}`)
- }
-}
-
-function isRx<A>(x: any): x is Rx<A> {
- return x !== undefined && x.type !== undefined && (x.type === "Var" || x.type === "Map" || x.type === "FlatMap")
-}
-
-function isVar<A>(x: any): x is Var<A> {
- return x.type === "Var"
-}
-
-function isMap<A, B>(x: any): x is Map<A, B> {
- return x.type === "Map"
-}
-
-function isFlatMap<A, B>(x: any): x is FlatMap<A, B> {
- return x.type === "FlatMap"
-}
-
-// Append
-
-interface AppendResult {
- cancel: Cancelable
- remove: Removable
- lastAdded?: Node
-}
-
-function appendChild(state: State, element: Element, child: Html, lastAdded?: Node): AppendResult {
- if (Array.isArray(child)) {
- let cancels: Array<Cancelable> = []
- let removes: Array<Removable> = []
- child.forEach((o) => {
- const appendResult = appendChild(state, element, o, lastAdded)
- cancels.push(appendResult.cancel)
- removes.push(appendResult.remove)
- lastAdded = appendResult.lastAdded
- })
- return {
- cancel: () => cancels.forEach((o) => o()),
- remove: () => removes.forEach((o) => o()),
- lastAdded
- }
- } else if (typeof child == "string") {
- const node = document.createTextNode(child)
- appendNode(element, node, lastAdded)
- return {
- cancel: voidCancel,
- remove: () => element.removeChild(node),
- lastAdded: node
- }
- } else if (typeof child == "number") {
- return appendChild(state, element, child.toString(), lastAdded)
- } else if (isTag(child)) {
- const { tagName, attributes, children, onmount, onunmount } = child
-
- const childElement = document.createElement(tagName)
- const cancelAttributes = Object.entries(attributes).map(([key, value]) => {
- if (isRx<AttributeValue>(value)) {
- return rxRun(state, value, newValue => setAttribute(state, childElement, key, newValue))
- } else {
- setAttribute(state, childElement, key, value)
- }
- })
-
- const appendChildrenRes = appendChild(state, childElement, children)
-
- appendNode(element, childElement, lastAdded)
-
- if (onmount !== undefined) {
- onmount(childElement)
- }
-
- return {
- cancel: () => {
- cancelAttributes.forEach(cancel => cancel !== undefined ? cancel() : {})
- appendChildrenRes.cancel()
- if (onunmount !== undefined) {
- onunmount(childElement)
- }
- },
- remove: () => element.removeChild(childElement),
- lastAdded: childElement,
- }
- } else if (isWithVar(child)) {
- const { init, getChildren } = child
- const v = state.register(init)
- const children = getChildren(v, f => state.update(v, f))
- const appendRes = appendChild(state, element, children)
- return {
- cancel: () => {
- appendRes.cancel()
- state.unregister(v)
- },
- remove: () => appendRes.remove(),
- lastAdded: appendRes.lastAdded
- }
- } else if (isRx(child)) {
- const rxBase = document.createTextNode('')
- appendNode(element, rxBase, lastAdded)
- let appendRes: AppendResult = {
- cancel: voidCancel,
- remove: voidRemove,
- lastAdded: rxBase
- }
- const cancelRx = rxRun(state, child, (value: Html) => {
- appendRes.cancel()
- appendRes.remove()
- appendRes = appendChild(state, element, value, rxBase)
- })
- return {
- cancel: () => {
- appendRes.cancel()
- cancelRx()
- },
- remove: () => {
- appendRes.remove()
- element.removeChild(rxBase)
- },
- lastAdded: appendRes.lastAdded,
- }
- } else if (child === undefined || child === false) {
- return {
- cancel: voidCancel,
- remove: voidRemove,
- lastAdded
- }
- } else {
- throw new Error(`Unrecognized child: ${child}`)
- }
-}
-
-function isTag<A>(x: any): x is Tag {
- return x !== undefined && x.type === "Tag"
-}
-
-function isWithVar<A>(x: any): x is WithVar<A> {
- return x !== undefined && x.type === "WithVar"
-}
-
-function appendNode(base: Element, node: Node, lastAdded?: Node) {
- if (lastAdded !== undefined) {
- base.insertBefore(node, lastAdded.nextSibling)
- } else {
- base.append(node)
- }
-}
-
-function setAttribute(state: State, element: Element, key: string, attribute: AttributeValue) {
- if (typeof attribute == "boolean") {
- if (attribute) {
- // @ts-ignore
- element[key] = "true"
- }
- } else if (typeof attribute == "number") {
- // @ts-ignore
- element[key] = attribute.toString()
- } else if (typeof attribute == "string") {
- // @ts-ignore
- element[key] = attribute
- } else {
- // @ts-ignore
- element[key] = (event: Event) => attribute(event)
- }
-}
diff --git a/src/view/client/lib/search.ts b/src/view/client/lib/search.ts
deleted file mode 100644
index 026cb94..0000000
--- a/src/view/client/lib/search.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export function match(search: string, ...targets: Array<string | undefined>): boolean {
- const formattedTargets = targets
- .filter(t => t !== undefined)
- .map(format)
-
- return search.split(/\s+/).every(subSearch =>
- formattedTargets.some(target => target.includes(format(subSearch)))
- )
-}
-
-export function format(str: string): string {
- return unaccent(str.toLowerCase())
-}
-
-function unaccent(str: string): string {
- return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
-}
diff --git a/src/view/client/main.ts b/src/view/client/main.ts
deleted file mode 100644
index 5885871..0000000
--- a/src/view/client/main.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { h, withVar, mount, Html } from 'lib/rx'
-import * as Search from 'lib/search'
-import * as Functions from 'lib/functions'
-import * as I18n from 'lib/i18n'
-import * as Filters from 'view/filters'
-import * as Books from 'view/books'
-import * as Book from 'book'
-
-// @ts-ignore
-const sortedBookLibrary: Array<Book> = (bookLibrary as Array<Book.Book>).sort((a, b) =>
- a.authorsSort == b.authorsSort
- ? a.date > b.date
- : a.authorsSort > b.authorsSort)
-
-mount(withVar<Filters.Model>({}, (filters, updateFilters) =>
- withVar('', (search, updateSearch) => {
- const filteredBooks = filters.flatMap(f => search.map(s =>
- sortedBookLibrary.filter(book =>
- (f.read === undefined || book.read === f.read)
- && (s === '' || matchSearch(book, s))
- )
- ))
-
- return [
- h('aside', Filters.view({ filteredBooks, filters, updateFilters })),
- h('main',
- h('header',
- h('input',
- { className: 'g-Search',
- oninput: Functions.debounce(
- (event: Event) => updateSearch(_ => (event.target as HTMLInputElement).value),
- 500
- )
- }
- ),
- filteredBooks.map(fb => I18n.unit(fb.length, 'livre', 'livres'))
- ),
- Books.view(filteredBooks)
- )
- ]
- })
-))
-
-function matchSearch(book: Book.Book, search: string): boolean {
- return Search.match(search, book.title, book.subtitle, ...book.authors)
-}
diff --git a/src/view/client/view/books.ts b/src/view/client/view/books.ts
deleted file mode 100644
index aba55c1..0000000
--- a/src/view/client/view/books.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { h, withVar, mount, Html, Rx } from 'lib/rx'
-import * as Book from 'book'
-import * as Modal from 'view/components/modal'
-
-export function view(books: Rx<Array<Book.Book>>): Html {
- return h('div',
- { className: 'g-Books' },
- withVar<Book.Book | undefined>(undefined, (focusBook, updateFocusBook) => [
- books.map(bs => [
- focusBook.map(book => {
- if (book !== undefined) {
- let onKeyup = keyupHandler({
- books: bs,
- book,
- onUpdate: (book: Book.Book) => updateFocusBook(_ => book)
- })
-
- return bookDetailModal({
- book,
- onClose: () => updateFocusBook(_ => undefined),
- onmount: () => addEventListener('keyup', onKeyup),
- onunmount: () => removeEventListener('keyup', onKeyup)
- })
- }
- }),
- bs.map(book => viewBook({
- book,
- onSelect: (book) => updateFocusBook(_ => book)
- }))
- ])
- ])
- )
-}
-
-interface KeyupHandlerParams {
- books: Array<Book.Book>
- book: Book.Book
- onUpdate: (book: Book.Book) => void
-}
-
-function keyupHandler({ books, book, onUpdate }: KeyupHandlerParams): ((e: KeyboardEvent) => void) {
- return (e: KeyboardEvent) => {
- if (e.key === 'ArrowLeft') {
- const indexedBooks = books.map((b, i) => ({ b, i }))
- const focus = indexedBooks.find(({ b }) => b == book)
- if (focus !== undefined && focus.i > 0) {
- onUpdate(books[focus.i - 1])
- }
- } else if (e.key === 'ArrowRight') {
- const indexedBooks = books.map((b, i) => ({ b, i }))
- const focus = indexedBooks.find(({ b }) => b == book)
- if (focus !== undefined && focus.i < books.length - 1) {
- onUpdate(books[focus.i + 1])
- }
- }
- }
-}
-
-interface ViewBookParams {
- book: Book.Book
- onSelect: (book: Book.Book) => void
-}
-
-function viewBook({ book, onSelect }: ViewBookParams): Html {
- return h('button',
- { className: 'g-Book' },
- h('img',
- { src: book.cover,
- alt: book.title,
- className: 'g-Book__Image',
- onclick: () => onSelect(book)
- }
- )
- )
-}
-
-interface BookDetailModalParams {
- book: Book.Book
- onClose: () => void
- onmount: () => void
- onunmount: () => void
-}
-
-function bookDetailModal({ book, onClose, onmount, onunmount }: BookDetailModalParams): Html {
- return Modal.view({
- header: h('div',
- h('div', { className: 'g-BookDetail__Title' }, `${book.title}, ${book.date}`),
- book.subtitle && h('div', { className: 'g-BookDetail__Subtitle' }, book.subtitle)
- ),
- body: h('div',
- { className: 'g-BookDetail' },
- h('img', { src: book.cover }),
- h('div',
- h('dl',
- metadata('Auteur', book.authors),
- metadata('Genre', book.genres)
- ),
- book.summary && book.summary
- .split('\n')
- .map(str => str.trim())
- .filter(str => str != '')
- .map(str => h('p', str))
- )
- ),
- onClose,
- onmount,
- onunmount
- })
-}
-
-function metadata(term: string, descriptions: Array<string>): Html {
- return h('div',
- h('dt', term, descriptions.length > 1 && 's', ' :'),
- h('dd', ' ', descriptions.join(', '))
- )
-}
diff --git a/src/view/client/view/components/modal.ts b/src/view/client/view/components/modal.ts
deleted file mode 100644
index 5e845e1..0000000
--- a/src/view/client/view/components/modal.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { h, Html } from 'lib/rx'
-
-interface Params {
- header: Html
- body: Html
- onClose: () => void
- onmount?: (element: Element) => void
- onunmount?: (element: Element) => void
-}
-
-export function view({ header, body, onClose, onmount, onunmount }: Params): Html {
- return h('div',
- { className: 'g-Modal',
- onclick: () => onClose(),
- onmount: (element: Element) => onmount && onmount(element),
- onunmount: (element: Element) => onunmount && onunmount(element)
- },
- h('div',
- { className: 'g-Modal__Content',
- onclick: (e: Event) => e.stopPropagation()
- },
- h('div',
- { className: 'g-Modal__Header' },
- header,
- h('button',
- { className: 'g-Modal__Close',
- onclick: () => onClose()
- },
- '✕'
- )
- ),
- h('div',
- { className: 'g-Modal__Body' },
- body
- )
- )
- )
-}
diff --git a/src/view/client/view/filters.ts b/src/view/client/view/filters.ts
deleted file mode 100644
index efe4115..0000000
--- a/src/view/client/view/filters.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { h, Rx, Html } from 'lib/rx'
-import * as Book from 'book'
-import * as I18n from 'lib/i18n'
-
-// Model
-
-export interface Model {
- read?: Book.ReadStatus
-}
-
-const init: Model = {}
-
-// View
-
-interface ViewFiltersParams {
- filteredBooks: Rx<Array<Book.Book>>
- filters: Rx<Model>
- updateFilters: (f: (filters: Model) => Model) => void
-}
-
-export function view({ filteredBooks, filters, updateFilters }: ViewFiltersParams): Html {
- return h('ul',
- h('li', [
- h('div', { className: 'g-FilterTitle' }, 'Lecture'),
- readFilter({
- filteredBooks,
- readStatus: filters.map(fs => fs.read),
- update: (status?: Book.ReadStatus) => updateFilters(fs => {
- fs.read = status
- return fs
- })
- })
- ])
- )
-}
-
-interface ReadFilterParams {
- filteredBooks: Rx<Array<Book.Book>>
- readStatus: Rx<Book.ReadStatus | undefined>
- update: (status?: Book.ReadStatus) => void
-}
-
-function readFilter({ filteredBooks, readStatus, update }: ReadFilterParams): Html {
- return h('ul',
- { className: 'g-Filters' },
- readStatus.map(currentStatus => {
- if (currentStatus !== undefined) {
- return h('li',
- { className: 'g-Filter g-Filter--Selected' },
- h('button',
- { onclick: () => update(undefined) },
- filteredBooks.map(xs => unit(xs.length, readStatusLabels(currentStatus)))
- )
- )
- } else {
- return Book.readStatuses.map(status =>
- filteredBooks.map(xs => {
- const count = xs.filter(b => b.read === status).length
-
- return count !== 0
- ? h('li',
- { className: 'g-Filter g-Filter--Unselected' },
- h('button',
- { onclick: () => update(status) },
- unit(count, readStatusLabels(status))
- )
- )
- : undefined
- })
- )
- }
- })
- )
-}
-
-function unit(n: number, labels: Array<string>): string {
- return I18n.unit(n, labels[0], labels[1], (n, str) => `${str} (${n})`)
-}
-
-function readStatusLabels(status: Book.ReadStatus): Array<string> {
- if (status === 'Read') {
- return ['lu', 'lus']
- } else if (status === 'Unread') {
- return ['non lu', 'non lus']
- } else if (status === 'Reading') {
- return ['en cours', 'en cours']
- } else {
- return ['arrêté', 'arrêtés']
- }
-}
diff --git a/src/view/command.py b/src/view/command.py
deleted file mode 100644
index e50027f..0000000
--- a/src/view/command.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import os
-import shutil
-import subprocess
-import tempfile
-import time
-
-import library.command
-
-def run(book_library, browser_cmd):
- tmp_dir = tempfile.mkdtemp()
- directory = os.path.dirname(os.path.realpath(__file__))
- shutil.copytree(f'{directory}/public', tmp_dir, dirs_exist_ok=True)
- with open(f'{tmp_dir}/books.js', 'w') as f:
- json = library.command.get(book_library)
- f.write(f'const bookLibrary = {json}')
- browser_cmd = f'{browser_cmd} {tmp_dir}/index.html'
- subprocess.run(browser_cmd.split(' '))
diff --git a/src/view/public/index.html b/src/view/public/index.html
deleted file mode 100644
index ce4d568..0000000
--- a/src/view/public/index.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!DOCTYPE html>
-<meta charset="UTF-8">
-<link rel="stylesheet" href="main.css">
-<title>Bibliothèque</title>
-<body></body>
-<script src="books.js"></script>
-<script src="main.js"></script>
diff --git a/src/view/public/main.css b/src/view/public/main.css
deleted file mode 100644
index f361cbe..0000000
--- a/src/view/public/main.css
+++ /dev/null
@@ -1,235 +0,0 @@
-/* Variables */
-
-:root {
- --color-focus: #888833;
-
- --font-size-dog: 1.5rem;
-
- --spacing-mouse: 0.25rem;
- --spacing-cat: 0.5rem;
- --spacing-dog: 1rem;
- --spacing-horse: 2rem;
-
- --width-close-button: 3rem;
- --width-book: 13rem;
-}
-
-/* Top level */
-
-html {
- height: 100%;
-}
-
-body {
- margin: 0;
- display: flex;
- height: 100%;
- font-family: sans-serif;
-}
-
-dl {
- margin: 0;
-}
-
-dd {
- margin-left: 0;
-}
-
-p {
- margin: 0;
-}
-
-/* Modal */
-
-.g-Modal {
- width: 100vw;
- height: 100vh;
- position: fixed;
- top: 0;
- left: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: rgba(0, 0, 0, 0.5);
- z-index: 1;
-}
-
-.g-Modal__Content {
- position: relative;
- background-color: white;
- width: 50%;
- border-radius: 0.2rem;
- border: 1px solid #EEE;
-}
-
-.g-Modal__Header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--spacing-dog) var(--spacing-horse);
- border-bottom: 1px solid #EEE;
-}
-
-.g-Modal__Body {
- padding: var(--spacing-horse);
- max-height: 50vh;
- overflow-y: scroll;
-}
-
-.g-Modal__Close {
- cursor: pointer;
- font-weight: bold;
- border-radius: 50%;
- border: 1px solid #EEE;
- background-color: transparent;
- width: var(--width-close-button);
- height: var(--width-close-button);
- font-size: 1.7rem;
- flex-shrink: 0;
-}
-
-.g-Modal__Close:hover, .g-Modal__Close:focus {
- background-color: #EEE;
-}
-
-/* Filters */
-
-aside {
- background-color: #333;
- color: white;
- width: 200px;
- overflow-y: auto;
-}
-
-ul {
- margin: 0;
- padding: 0;
- list-style-type: none;
-}
-
-.g-FilterTitle {
- padding: 0.5rem 1rem;
- background-color: #555;
- border-left: 8px solid transparent;
- font-weight: bold;
-}
-
-.g-Filter--Unselected {
- border-left: 8px solid #555;
-}
-
-.g-Filter--Selected {
- border-left: 8px solid #883333;
-}
-
-.g-Filter button {
- border: none;
- background-color: transparent;
- color: inherit;
- cursor: pointer;
- padding: 0.5rem 1rem;
- width: 100%;
- text-align: left;
-}
-
-.g-Filter button:hover {
- background-color: var(--color-focus);
-}
-
-/* Books */
-
-main {
- width: 100%;
- padding: 1rem;
- overflow-y: auto;
-}
-
-header {
- display: flex;
- font-size: 120%;
- margin-bottom: 1rem;
-}
-
-.g-Search {
- margin-right: 1rem;
-}
-
-.g-Books {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(var(--width-book), 1fr));
- grid-gap: 1rem;
-}
-
-.g-Book {
- align-self: center;
- border: 1px solid #DDDDDD;
- padding: 0;
- width: fit-content;
-}
-
-.g-Book:hover {
- cursor: pointer;
- outline: none;
-}
-
-.g-Book:hover .g-Book__Image {
- transform: scale(105%);
- opacity: 0.9;
-}
-
-.g-Book__Image {
- display: flex;
- width: var(--width-book);
- transition: all 0.2s ease-in-out;
-}
-
-/* Book detail */
-
-.g-BookDetail {
- display: flex;
- align-items: flex-start;
-}
-
-.g-BookDetail img {
- width: var(--width-book);
- margin-right: var(--spacing-horse);
- border: 1px solid #EEE;
-}
-
-.g-BookDetail__Title {
- font-size: var(--font-size-dog);
- margin-bottom: var(--spacing-mouse);
-}
-
-.g-BookDetail__Subtitle {
- font-style: italic;
- margin-bottom: var(--spacing-mouse);
-}
-
-.g-BookDetail dl {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-cat);
- margin-bottom: var(--spacing-dog);
-}
-
-.g-BookDetail dt {
- display: inline;
- text-decoration: underline;
-}
-
-.g-BookDetail dd {
- display: inline;
-}
-
-.g-BookDetail p {
- font-family: serif;
- text-align: justify;
- line-height: 1.4rem;
- font-size: 1.1rem;
- text-indent: 1.5rem;
-}
-
-.g-BookDetail p:not(:last-child) {
- margin-bottom: var(--spacing-dog);
-}
diff --git a/src/view/tsconfig.json b/src/view/tsconfig.json
deleted file mode 100644
index 1e07c37..0000000
--- a/src/view/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "module": "amd",
- "target": "es2020",
- "baseUrl": "client",
- "outFile": "public/main.js",
- "noImplicitAny": true,
- "strictNullChecks": true,
- "removeComments": true,
- "preserveConstEnums": true
- },
- "include": ["client/**/*"]
-}