// ==PREPROCESSOR== // @import "%fb2k_component_path%helpers.txt" // ==/PREPROCESSOR== // ===================================================================== // AUDIOBOOK PANEL dla foobar2000 v2 (64-bit) // Komponent: JScript Panel 3 (foo_jscript_panel3) - API DirectWrite // Jezyk silnika: JScript (ES5) -> uzywamy var / function, bez ES6. // --------------------------------------------------------------------- // Import / wyswietlanie / motywy / wznawianie / playlista = tytul. // Rysowanie: gr.FillRectangle / gr.WriteText / gr.DrawImage // Czcionki: CreateFontString(family, size) (z helpers.txt) // Obrazy: utils.LoadImage(path) // ===================================================================== // ----------------------- HELPERY KOLOR ------------------------------- function RGB(r, g, b) { return (0xff000000 | (r << 16) | (g << 8) | (b)); } function RGBA(r, g, b, a) { return ((a << 24) | (r << 16) | (g << 8) | (b)); } // wyrownanie tekstu dla gr.WriteText (h_align / v_align): 0 = start, 1 = center, 2 = end var AL = 0, AC = 1, AR = 2; function wt(gr, s, font, col, x, y, w, h, ha, va) { try { gr.WriteText(s, font, col, x, y, w, h, ha || 0, va || 0); } catch (e) {} } // Globalny komunikat bledu (pokazywany na panelu, zeby nigdy nie byl pusty) var initError = ""; function trace(msg) { try { console.log("[Audiobook] " + msg); } catch (e) {} try { fb.trace("[Audiobook] " + msg); } catch (e2) {} } // timer odporny na roznice API function setT(fn, ms) { try { return window.SetTimeout(fn, ms); } catch (e) { try { return setTimeout(fn, ms); } catch (e2) { trace("timer: " + e2); return null; } } } // ActiveX (FileSystemObject / Shell) - do skanowania folderow i okna wyboru var fso = null, shell = null; try { fso = new ActiveXObject("Scripting.FileSystemObject"); } catch (e) { trace("Brak FileSystemObject: " + e); } try { shell = new ActiveXObject("Shell.Application"); } catch (e) { trace("Brak Shell.Application: " + e); } if (!fso) initError = "Brak FileSystemObject (ActiveX). Import folderow nie zadziala - napisz mi to."; // ----------------------------- MOTYWY -------------------------------- var THEMES = [ { name: "Ciemny", bg: RGB(24,24,28), card: RGB(38,38,46), text: RGB(235,235,240), dim: RGB(150,150,160), accent: RGB(255,170,40), sel: RGB(60,70,95) }, { name: "Granat", bg: RGB(18,24,40), card: RGB(28,38,62), text: RGB(225,232,245), dim: RGB(130,150,185), accent: RGB(90,170,255), sel: RGB(40,60,100) }, { name: "Sepia", bg: RGB(40,34,28), card: RGB(58,50,40), text: RGB(240,232,215), dim: RGB(175,160,135), accent: RGB(220,160,90), sel: RGB(80,68,52) }, { name: "Jasny", bg: RGB(245,245,248),card: RGB(255,255,255),text: RGB(30,30,35), dim: RGB(120,120,130), accent: RGB(220,120,20), sel: RGB(210,225,250) }, { name: "Las", bg: RGB(20,30,26), card: RGB(30,46,40), text: RGB(225,240,232), dim: RGB(135,170,155), accent: RGB(90,210,150), sel: RGB(40,70,58) } ]; var THEME_NAMES = { pl: ["Ciemny","Granat","Sepia","Jasny","Las"], en: ["Dark","Navy","Sepia","Light","Forest"] }; // --------------------------- JEZYK / LANGUAGE ------------------------ var STR = { pl: { file:"+ Plik", folder:"+ Folder", tree:"+ Drzewo", theme:"Motyw: ", list:"Lista", det:"Szczeg", icoS:"Ik-", icoL:"Ik+", lib:"< Biblioteka", empty:"Brak audiobookow. \"+ Plik\" = zaznaczony w playliscie mp3 \"+ Folder\" = jeden folder \"+ Drzewo\" = folder z podfolderami.", unknown:"Nieznany autor", chapters:"rozdzialow", chShort:"rozdz.", last:"Ostatnio: ", resume:"Wznow", play:"Odtwarzaj", fromStart:"Od poczatku", cover:"audiobook", mRefresh:"Odswiez biblioteke", mSortTitle:"Sortuj wg tytulu", mSortAuthor:"Sortuj wg autora", mClear:"Usun wszystkie zaimportowane", pickFiles:"Najpierw zaznacz w playliscie plik(i) mp3, potem kliknij \"+ Plik\". (1 plik = audiobook z 1 rozdzialem; kilka = rozdzialy.)", pickFolderSingle:"Wybierz folder JEDNEGO audiobooka (pliki mp3):", pickFolderTree:"Wybierz katalog z wieloma audiobookami (podkatalogi):" }, en: { file:"+ File", folder:"+ Folder", tree:"+ Tree", theme:"Theme: ", list:"List", det:"Details", icoS:"Ic-", icoL:"Ic+", lib:"< Library", empty:"No audiobooks. \"+ File\" = mp3 selected in playlist \"+ Folder\" = one folder \"+ Tree\" = folder with subfolders.", unknown:"Unknown author", chapters:"chapters", chShort:"ch.", last:"Last: ", resume:"Resume", play:"Play", fromStart:"From start", cover:"audiobook", mRefresh:"Refresh library", mSortTitle:"Sort by title", mSortAuthor:"Sort by author", mClear:"Remove all imported", pickFiles:"First select mp3 file(s) in the playlist, then click \"+ File\". (1 file = audiobook with 1 chapter; several = chapters.)", pickFolderSingle:"Choose a folder of ONE audiobook (mp3 files):", pickFolderTree:"Choose a folder with multiple audiobooks (subfolders):" } }; function L() { return (state.settings.lang === "en") ? "en" : "pl"; } function tr(k) { return STR[L()][k]; } function themeName(){ return THEME_NAMES[L()][state.settings.theme % THEMES.length]; } // --------------------------- KONFIG / STAN --------------------------- var STATE_DIR = (function () { try { return fb.ProfilePath + "js_audiobooks\\"; } catch (e) { return "js_audiobooks\\"; } })(); var STATE_FILE = STATE_DIR + "state.json"; var DEFAULT_STATE = { roots: [], customBooks: [], // ksiazki dodane z pojedynczych plikow / zaznaczenia settings: { theme: 0, view: 1, sortBy: "name", lang: "pl" }, // view: 0 lista,1 szczegoly,2 ikony male,3 ikony duze progress: {} // bookPath -> { chapter, position, title, updated } }; var state = clone(DEFAULT_STATE); var books = []; var bookByChapter = {}; var screen = "library"; // "library" | "book" var selectedBook = -1; var scroll = 0, maxScroll = 0; var ww = 0, hh = 0; var buttons = []; var artCache = {}; var nowPlayingPath = ""; var saveTick = 0; var pendingResume = null; var curPos = 0; // ostatnia znana pozycja odtwarzania (sek) - NIE zerowana przy stop var resuming = false; // true podczas wznawiania (blokuje zapis, by nie nadpisac 0) // czcionki jako "font strings" (helpers.txt -> CreateFontString) function mkFont(name, size) { try { return CreateFontString(name, size); } catch (e) { return ""; } } var fontTitle = mkFont("Segoe UI Semibold", 15); var fontBody = mkFont("Segoe UI", 13); var fontSmall = mkFont("Segoe UI", 11); var fontBig = mkFont("Segoe UI Semibold", 20); var ROW_H = 26, DETAIL_H = 56, TOOLBAR_H = 38; var hoverIndex = -1; var hoverBtn = -1; // pasek przewijania var dragging = false, dragOffset = 0; var sb = null; // ostatnia geometria: { x, w, top, viewH, thumbY, thumbH } var SB_W = 12; // szerokosc paska // ==================================================================== // UTIL // ==================================================================== function clone(o) { return JSON.parse(JSON.stringify(o)); } function theme() { return THEMES[state.settings.theme % THEMES.length]; } function lc(s) { return (s || "").toLowerCase(); } function repaint(){ try { window.Repaint(); } catch (e) {} } function clampScroll() { if (scroll < 0) scroll = 0; if (scroll > maxScroll) scroll = maxScroll; } // rysuje pasek przewijania po prawej; zapamietuje geometrie w 'sb' do obslugi myszy function drawScrollbar(gr, top, viewH, contentH) { sb = null; if (viewH <= 0) return; var t = theme(); var x = ww - SB_W; // tor zawsze widoczny gr.FillRectangle(x, top, SB_W, viewH, RGBA(128, 128, 128, 50)); var ratio = (contentH > viewH) ? (viewH / contentH) : 1; var thumbH = Math.max(28, Math.floor(viewH * ratio)); if (thumbH > viewH) thumbH = viewH; var thumbY = (maxScroll > 0) ? top + Math.floor((scroll / maxScroll) * (viewH - thumbH)) : top; // suwak: akcent gdy mozna przewijac, przygaszony gdy wszystko sie miesci gr.FillRoundedRectangle(x + 2, thumbY, SB_W - 4, thumbH, 4, 4, (maxScroll > 0) ? t.accent : t.dim); sb = { x: x, w: SB_W, top: top, viewH: viewH, thumbY: thumbY, thumbH: thumbH }; } function ensureDir(path) { try { if (fso && !fso.FolderExists(path)) fso.CreateFolder(path); } catch (e) {} } function readFile(path) { try { if (fso && !fso.FileExists(path)) return ""; } catch (e) {} try { return utils.ReadTextFile(path); } catch (e) {} try { var ts = fso.OpenTextFile(path, 1, false, -1); var s = ts.AtEndOfStream ? "" : ts.ReadAll(); ts.Close(); return s; } catch (e2) { return ""; } } function writeFile(path, content) { try { utils.WriteTextFile(path, content); return true; } catch (e) {} try { var ts = fso.CreateTextFile(path, true, true); ts.Write(content); ts.Close(); return true; } catch (e2) { trace("writeFile: " + e2); return false; } } function loadState() { var raw = ""; try { raw = window.GetProperty("audiobook_state", ""); } catch (e) {} // glowny magazyn (trwaly) if (!raw) raw = readFile(STATE_FILE); // zapas: plik if (raw) { try { var p = JSON.parse(raw); if (p.roots) state.roots = p.roots; if (p.customBooks) state.customBooks = p.customBooks; if (p.settings) state.settings = p.settings; if (p.progress) state.progress = p.progress; } catch (e) { trace("loadState parse: " + e); } } if (!state.customBooks) state.customBooks = []; if (state.settings.theme == null) state.settings.theme = 0; if (state.settings.view == null) state.settings.view = 1; if (state.settings.lang == null) state.settings.lang = "pl"; } function saveState() { var s = JSON.stringify(state); try { window.SetProperty("audiobook_state", s); } catch (e) { trace("SetProperty: " + e); } try { ensureDir(STATE_DIR); writeFile(STATE_FILE, s); } catch (e) {} // zapas: plik } function fmtTime(sec) { sec = Math.max(0, Math.floor(sec || 0)); var h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; function p(n) { return (n < 10 ? "0" : "") + n; } return (h > 0) ? (h + ":" + p(m) + ":" + p(s)) : (m + ":" + p(s)); } // ==================================================================== // SKAN / IMPORT // ==================================================================== var IMG_EXT = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp"]; function isImage(name) { var n = lc(name); for (var i = 0; i < IMG_EXT.length; i++) { var e = IMG_EXT[i]; if (n.length >= e.length && n.substr(n.length - e.length) === e) return true; } return false; } function isMp3(name) { var n = lc(name); return n.length > 4 && n.substr(n.length - 4) === ".mp3"; } function naturalCompare(a, b) { a = lc(a); b = lc(b); var ax = [], bx = []; a.replace(/(\d+)|(\D+)/g, function (_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]); }); b.replace(/(\d+)|(\D+)/g, function (_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]); }); while (ax.length && bx.length) { var an = ax.shift(), bn = bx.shift(); var nn = (an[0] - bn[0]) || (an[1] < bn[1] ? -1 : an[1] > bn[1] ? 1 : 0); if (nn) return nn; } return ax.length - bx.length; } function folderHasMp3(folder) { var e = new Enumerator(folder.Files); for (; !e.atEnd(); e.moveNext()) if (isMp3(e.item().Name)) return true; return false; } function buildBook(folder) { var chapters = [], cover = "", e; e = new Enumerator(folder.Files); for (; !e.atEnd(); e.moveNext()) { var f = e.item(); if (isMp3(f.Name)) { chapters.push({ path: f.Path, name: f.Name.replace(/\.[^.]+$/, "") }); } else if (!cover && isImage(f.Name)) { var nm = lc(f.Name); if (nm.indexOf("cover") === 0 || nm.indexOf("folder") === 0 || nm.indexOf("front") === 0) cover = f.Path; } } if (!cover) { e = new Enumerator(folder.Files); for (; !e.atEnd(); e.moveNext()) { if (isImage(e.item().Name)) { cover = e.item().Path; break; } } } chapters.sort(function (a, b) { return naturalCompare(a.name, b.name); }); var folderName = folder.Name, author = "", title = folderName; var dash = folderName.indexOf(" - "); if (dash > 0) { author = folderName.substr(0, dash); title = folderName.substr(dash + 3); } return { path: folder.Path, title: title, author: author, cover: cover, chapters: chapters }; } function scanForBooks(path, depth, out) { if (depth > 8 || !fso) return; var folder; try { folder = fso.GetFolder(path); } catch (e) { return; } // folder z mp3 = osobny audiobook; ZAWSZE schodzimy tez do podfolderow (rekurencyjnie) if (folderHasMp3(folder)) out.push(buildBook(folder)); try { var e = new Enumerator(folder.SubFolders); for (; !e.atEnd(); e.moveNext()) scanForBooks(e.item().Path, depth + 1, out); } catch (e2) {} } function rebuildLibrary() { books = []; bookByChapter = {}; for (var i = 0; i < state.roots.length; i++) scanForBooks(state.roots[i], 0, books); // ksiazki z pojedynczych plikow / zaznaczenia for (var j = 0; j < state.customBooks.length; j++) books.push(clone(state.customBooks[j])); books.sort(function (a, b) { if (state.settings.sortBy === "author") return naturalCompare(a.author + a.title, b.author + b.title); return naturalCompare(a.title, b.title); }); for (var b = 0; b < books.length; b++) for (var c = 0; c < books[b].chapters.length; c++) bookByChapter[lc(books[b].chapters[c].path)] = b; } function browseFolder(title) { if (!shell) { try { fb.ShowPopupMessage("Brak Shell.Application - nie moge otworzyc okna wyboru folderu.", "Audiobook Panel"); } catch (e) {} return ""; } try { var f = shell.BrowseForFolder(0, title, 0x10 + 0x40, 0); if (f && f.Self) return f.Self.Path; } catch (e) { trace("browseFolder: " + e); } return ""; } // + Folder: dokladnie JEDEN folder = jedna ksiazka (bez wchodzenia w podfoldery) function importSingle() { var p = browseFolder(tr("pickFolderSingle")); if (!p) return; var folder; try { folder = fso.GetFolder(p); } catch (e) { return; } for (var k = 0; k < state.customBooks.length; k++) if (lc(state.customBooks[k].path) === lc(p)) { rebuildLibrary(); repaint(); return; } var book = buildBook(folder); book.custom = true; if (!book.chapters.length) { try { fb.ShowPopupMessage("Brak plikow mp3 w wybranym folderze.", "Audiobook Panel"); } catch (e) {} return; } state.customBooks.push(book); saveState(); rebuildLibrary(); scroll = 0; repaint(); } // + Drzewo: caly katalog REKURENCYJNIE - kazdy podfolder z mp3 = osobna ksiazka function importDir() { var p = browseFolder(tr("pickFolderTree")); if (p) addRoot(p); } function addRoot(p) { for (var i = 0; i < state.roots.length; i++) if (lc(state.roots[i]) === lc(p)) { rebuildLibrary(); repaint(); return; } state.roots.push(p); saveState(); rebuildLibrary(); scroll = 0; repaint(); } // --- pojedynczy plik / zaznaczone pliki jako jeden audiobook --- function baseName(p) { try { return fso.GetBaseName(p); } catch (e) { var s = p.replace(/\\/g, "/"); s = s.substring(s.lastIndexOf("/") + 1); return s.replace(/\.[^.]+$/, ""); } } function parentFolderName(p) { try { return fso.GetBaseName(fso.GetParentFolderName(p)); } catch (e) { return ""; } } function findCoverNear(p) { try { var folder = fso.GetFolder(fso.GetParentFolderName(p)); var e = new Enumerator(folder.Files), first = ""; for (; !e.atEnd(); e.moveNext()) { var f = e.item(); if (isImage(f.Name)) { var nm = lc(f.Name); if (nm.indexOf("cover") === 0 || nm.indexOf("folder") === 0 || nm.indexOf("front") === 0) return f.Path; if (!first) first = f.Path; } } return first; } catch (e) { return ""; } } // importuje zaznaczone w aktywnej playliscie pliki mp3 (lub aktualnie wyrozniony) jako jeden audiobook function importFiles() { var paths = []; var handles = null; try { handles = plman.GetPlaylistSelectedItems(plman.ActivePlaylist); } catch (e) {} if (handles) { var cnt = 0; try { cnt = handles.Count; } catch (e) {} for (var i = 0; i < cnt; i++) { var h = null; try { h = handles.Item(i); } catch (e) { try { h = handles[i]; } catch (e2) {} } if (h && isMp3(h.Path)) paths.push(h.Path); } } if (!paths.length) { var fh = null; try { fh = fb.GetFocusItem(); } catch (e) {} if (fh && isMp3(fh.Path)) paths.push(fh.Path); } if (!paths.length) { try { fb.ShowPopupMessage(tr("pickFiles"), "Audiobook Panel"); } catch (e) {} return; } addCustomBook(paths); } function addCustomBook(paths) { paths.sort(function (a, b) { return naturalCompare(baseName(a), baseName(b)); }); var chapters = []; for (var i = 0; i < paths.length; i++) chapters.push({ path: paths[i], name: baseName(paths[i]) }); var first = paths[0]; var title = (paths.length === 1) ? baseName(first) : (parentFolderName(first) || "Audiobook"); var id = "file:" + lc(first); // unikaj duplikatu for (var k = 0; k < state.customBooks.length; k++) if (state.customBooks[k].path === id) { rebuildLibrary(); repaint(); return; } state.customBooks.push({ path: id, title: title, author: "", cover: findCoverNear(first), chapters: chapters, custom: true }); saveState(); rebuildLibrary(); screen = "library"; scroll = 0; repaint(); } // ==================================================================== // OKLADKI // ==================================================================== function getArt(path) { if (!path) return null; if (artCache.hasOwnProperty(path)) return artCache[path]; var img = null; try { img = utils.LoadImage(path); } catch (e) { img = null; } artCache[path] = img; return img; } function drawCover(gr, book, x, y, w, h) { var t = theme(); gr.FillRectangle(x, y, w, h, t.card); var img = getArt(book.cover); if (img) { var iw = img.Width, ih = img.Height; var scale = Math.max(w / iw, h / ih); var sx = (iw - w / scale) / 2, sy = (ih - h / scale) / 2; try { gr.DrawImage(img, x, y, w, h, sx, sy, w / scale, h / scale); } catch (e) { try { gr.DrawImage(img, x, y, w, h, 0, 0, iw, ih); } catch (e2) {} } } else { wt(gr, tr("cover"), fontSmall, t.dim, x, y, w, h, AC, AC); } } // ==================================================================== // ODTWARZANIE / PLAYLISTA / WZNAWIANIE // ==================================================================== function findOrCreatePlaylist(name) { try { var idx = plman.FindOrCreatePlaylist(name, false); if (idx >= 0) return idx; } catch (e) {} for (var i = 0; i < plman.PlaylistCount; i++) if (plman.GetPlaylistName(i) === name) return i; return plman.CreatePlaylist(plman.PlaylistCount, name); } function playlistCount(pl) { try { return plman.PlaylistItemCount(pl); } catch (e) {} try { return plman.GetPlaylistItems(pl).Count; } catch (e2) { return 0; } } function playBook(book, chapterIndex, position) { if (!book || !book.chapters.length) return; var name = book.title || "Audiobook"; var pl = findOrCreatePlaylist(name); try { plman.ClearPlaylist(pl); } catch (e) {} var paths = []; for (var i = 0; i < book.chapters.length; i++) paths.push(book.chapters[i].path); try { plman.AddLocations(pl, paths, false); } catch (e) { trace("AddLocations: " + e); } try { plman.ActivePlaylist = pl; } catch (e) {} var ci = (chapterIndex == null) ? 0 : chapterIndex; var pos = (position == null) ? 0 : position; pendingResume = { path: lc(book.chapters[ci].path), position: pos }; resuming = (pos > 0); // blokuj zapis, dopoki nie doskoczymy do pozycji waitAndPlay(pl, book.chapters.length, ci, 0); } function waitAndPlay(pl, expected, chapterIndex, tries) { var cnt = playlistCount(pl); if (cnt >= expected || tries > 40) { try { plman.ExecutePlaylistDefaultAction(pl, chapterIndex); } catch (e) { trace("ExecuteDefaultAction: " + e); } return; } setT(function () { waitAndPlay(pl, expected, chapterIndex, tries + 1); }, 80); } function resumeBook(book) { var pr = state.progress[book.path]; if (pr) playBook(book, pr.chapter || 0, pr.position || 0); else playBook(book, 0, 0); } function saveProgress(force, posOverride) { if (!nowPlayingPath) return; var bi = bookByChapter[lc(nowPlayingPath)]; if (bi == null) return; var pos = (posOverride != null) ? posOverride : Math.floor(fb.PlaybackTime); if (pos < 0) pos = 0; var book = books[bi], ci = 0; for (var i = 0; i < book.chapters.length; i++) if (lc(book.chapters[i].path) === lc(nowPlayingPath)) { ci = i; break; } state.progress[book.path] = { chapter: ci, position: pos, title: book.title, updated: (new Date()).getTime() }; if (force) saveState(); } // ==================================================================== // RYSOWANIE // ==================================================================== function addButton(label, x, y, w, h, action, active) { buttons.push({ label: label, x: x, y: y, w: w, h: h, action: action, active: !!active }); } function drawButton(gr, btn) { var t = theme(); gr.FillRectangle(btn.x, btn.y, btn.w, btn.h, btn.active ? t.accent : t.card); var col = btn.active ? t.bg : t.text; wt(gr, btn.label, fontBody, col, btn.x + 4, btn.y, btn.w - 8, btn.h, AC, AC); } function drawToolbar(gr) { var t = theme(); gr.FillRectangle(0, 0, ww, TOOLBAR_H, t.card); var x = 6, y = 6, h = TOOLBAR_H - 12; if (screen === "library") { addButton(tr("file"), x, y, 64, h, "importFiles"); x += 70; // pojedynczy/zaznaczone mp3 addButton(tr("folder"), x, y, 74, h, "importSingle"); x += 80; // jeden folder = jedna ksiazka addButton(tr("tree"), x, y, 74, h, "importDir"); x += 80; // folder z podfolderami addButton(tr("theme") + themeName(), x, y, 118, h, "theme"); x += 124; addButton(L() === "en" ? "EN" : "PL", x, y, 34, h, "lang"); var vx = ww - 6 - 4 * 40; addButton(tr("list"), vx, y, 36, h, "view0", state.settings.view === 0); vx += 40; addButton(tr("det"), vx, y, 36, h, "view1", state.settings.view === 1); vx += 40; addButton(tr("icoS"), vx, y, 36, h, "view2", state.settings.view === 2); vx += 40; addButton(tr("icoL"), vx, y, 36, h, "view3", state.settings.view === 3); } else { addButton(tr("lib"), x, y, 110, h, "back"); x += 116; addButton(L() === "en" ? "EN" : "PL", ww - 6 - 34, y, 34, h, "lang"); addButton(tr("theme") + themeName(), ww - 6 - 34 - 8 - 118, y, 118, h, "theme"); } for (var i = 0; i < buttons.length; i++) drawButton(gr, buttons[i]); } function progressPct(book) { var pr = state.progress[book.path]; if (!pr || !book.chapters.length) return 0; return Math.min(1, (pr.chapter + 0.5) / book.chapters.length); } function drawLibrary(gr) { var t = theme(); var top = TOOLBAR_H; gr.FillRectangle(0, top, ww, hh - top, t.bg); if (!books.length) { wt(gr, tr("empty"), fontBody, t.dim, 24, top + 24, ww - 48, hh - top - 48, AC, AC); maxScroll = 0; return; } var view = state.settings.view; if (view === 0) drawListMode(gr, top, false); else if (view === 1) drawListMode(gr, top, true); else drawIconMode(gr, top, view === 3); } function drawListMode(gr, top, details) { var t = theme(); var rowH = details ? DETAIL_H : ROW_H; var totalH = books.length * rowH; maxScroll = Math.max(0, totalH - (hh - top)); if (scroll > maxScroll) scroll = maxScroll; var y0 = top - scroll; for (var i = 0; i < books.length; i++) { var y = y0 + i * rowH; if (y + rowH < top || y > hh) continue; var book = books[i]; if (i === hoverIndex) gr.FillRectangle(0, y, ww, rowH, t.sel); if (details) { var cs = rowH - 10; drawCover(gr, book, 6, y + 5, cs, cs); var tx = cs + 16; wt(gr, book.title, fontTitle, t.text, tx, y + 6, ww - tx - 60, 22, AL, AC); wt(gr, (book.author || tr("unknown")) + " - " + book.chapters.length + " " + tr("chShort"), fontSmall, t.dim, tx, y + 28, ww - tx - 60, 18, AL, AC); var pct = progressPct(book); if (pct > 0) { gr.FillRectangle(tx, y + rowH - 9, ww - tx - 16, 4, t.card); gr.FillRectangle(tx, y + rowH - 9, (ww - tx - 16) * pct, 4, t.accent); } } else { wt(gr, book.title, fontBody, t.text, 12, y, ww - 90, rowH, AL, AC); var pc = Math.round(progressPct(book) * 100); if (pc > 0) wt(gr, pc + "%", fontSmall, t.accent, ww - 56 - SB_W, y, 48, rowH, AR, AC); } } drawScrollbar(gr, top, hh - top, totalH); } function drawIconMode(gr, top, big) { var t = theme(); var pad = 12, cell = big ? 150 : 92, labelH = big ? 38 : 28; var cols = Math.max(1, Math.floor((ww - pad - SB_W) / (cell + pad))); var rows = Math.ceil(books.length / cols); var rowH = cell + labelH + pad; maxScroll = Math.max(0, rows * rowH + pad - (hh - top)); if (scroll > maxScroll) scroll = maxScroll; var startX = Math.floor((ww - SB_W - cols * (cell + pad) + pad) / 2); for (var i = 0; i < books.length; i++) { var col = i % cols, row = Math.floor(i / cols); var x = startX + col * (cell + pad); var y = top + pad + row * rowH - scroll; if (y + rowH < top || y > hh) continue; var book = books[i]; if (i === hoverIndex) gr.FillRectangle(x - 4, y - 4, cell + 8, rowH, t.sel); drawCover(gr, book, x, y, cell, cell); var pct = progressPct(book); if (pct > 0) { gr.FillRectangle(x, y + cell - 5, cell, 5, RGBA(0,0,0,150)); gr.FillRectangle(x, y + cell - 5, cell * pct, 5, t.accent); } wt(gr, book.title, fontSmall, t.text, x, y + cell + 2, cell, labelH - 16, AC, AL); if (big) wt(gr, book.author || "", fontSmall, t.dim, x, y + cell + labelH - 16, cell, 14, AC, AC); } drawScrollbar(gr, top, hh - top, rows * rowH + pad); } function drawBook(gr) { var t = theme(); var top = TOOLBAR_H; gr.FillRectangle(0, top, ww, hh - top, t.bg); var book = books[selectedBook]; if (!book) { screen = "library"; return; } var coverSize = Math.min(180, Math.floor(ww / 3)); var x = 16, y = top + 16; drawCover(gr, book, x, y, coverSize, coverSize); var tx = x + coverSize + 18, tw = ww - tx - 16; wt(gr, book.title, fontBig, t.text, tx, y, tw, 30, AL, AC); wt(gr, book.author || tr("unknown"), fontBody, t.dim, tx, y + 34, tw, 20, AL, AC); wt(gr, book.chapters.length + " " + tr("chapters"), fontSmall, t.dim, tx, y + 58, tw, 18, AL, AC); var pr = state.progress[book.path]; if (pr) { var chName = (book.chapters[pr.chapter] ? book.chapters[pr.chapter].name : (tr("chShort") + " " + (pr.chapter + 1))); wt(gr, tr("last") + chName + " @ " + fmtTime(pr.position), fontSmall, t.accent, tx, y + 78, tw, 18, AL, AC); } var by = y + coverSize - 36, bx = tx, bw = 150, bh = 32; addButton(pr ? tr("resume") : tr("play"), bx, by, bw, bh, "resume", true); addButton(tr("fromStart"), bx + bw + 8, by, 140, bh, "fromStart"); drawButton(gr, buttons[buttons.length - 2]); drawButton(gr, buttons[buttons.length - 1]); var listTop = y + coverSize + 16; gr.FillRectangle(0, listTop - 6, ww, 1, t.card); var totalH = book.chapters.length * ROW_H; maxScroll = Math.max(0, totalH - (hh - listTop)); if (scroll > maxScroll) scroll = maxScroll; var curCh = -1; if (nowPlayingPath) for (var k = 0; k < book.chapters.length; k++) if (lc(book.chapters[k].path) === lc(nowPlayingPath)) { curCh = k; break; } var y0 = listTop - scroll; for (var i = 0; i < book.chapters.length; i++) { var ry = y0 + i * ROW_H; if (ry + ROW_H < listTop || ry > hh) continue; if (i === hoverIndex) gr.FillRectangle(0, ry, ww, ROW_H, t.sel); var isCur = (i === curCh); var col = isCur ? t.accent : t.text; wt(gr, (i + 1) + ".", fontSmall, t.dim, 12, ry, 36, ROW_H, AR, AC); wt(gr, (isCur ? "> " : "") + book.chapters[i].name, fontBody, col, 56, ry, ww - 70 - SB_W, ROW_H, AL, AC); } drawScrollbar(gr, listTop, hh - listTop, totalH); } // ==================================================================== // CALLBACKI // ==================================================================== function on_paint(gr) { try { var t = theme(); gr.FillRectangle(0, 0, ww, hh, t.bg); buttons = []; if (screen === "library") { drawLibrary(gr); drawToolbar(gr); } else { drawBook(gr); drawToolbar(gr); } if (initError) { gr.FillRectangle(0, hh - 60, ww, 60, RGBA(120, 30, 30, 230)); wt(gr, initError, fontSmall, RGB(255, 230, 230), 10, hh - 56, ww - 20, 52, AL, AL); } } catch (err) { try { gr.FillRectangle(0, 0, ww, hh, RGB(30, 20, 20)); wt(gr, "BLAD RYSOWANIA: " + err, fontBody, RGB(255, 200, 200), 12, 12, ww - 24, hh - 24, AL, AL); } catch (e2) {} trace("on_paint: " + err); } } function on_size() { ww = window.Width; hh = window.Height; } function scrollToThumbY(ty) { if (!sb) return; var span = sb.viewH - sb.thumbH; scroll = (span > 0) ? ((ty - sb.top) / span) * maxScroll : 0; clampScroll(); } function on_mouse_lbtn_down(x, y) { if (sb && x >= sb.x && x <= sb.x + sb.w && y >= sb.top && y <= sb.top + sb.viewH) { if (y >= sb.thumbY && y <= sb.thumbY + sb.thumbH) { dragging = true; dragOffset = y - sb.thumbY; } else { dragging = true; dragOffset = sb.thumbH / 2; scrollToThumbY(y - dragOffset); repaint(); } } } function on_mouse_move(x, y) { if (dragging && sb) { scrollToThumbY(y - dragOffset); repaint(); return; } var oldH = hoverIndex, oldB = hoverBtn; hoverBtn = -1; for (var i = 0; i < buttons.length; i++) { var b = buttons[i]; if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) { hoverBtn = i; break; } } hoverIndex = (hoverBtn >= 0) ? -1 : hitItem(x, y); if (hoverIndex !== oldH || hoverBtn !== oldB) repaint(); } function on_mouse_leave() { hoverIndex = -1; hoverBtn = -1; repaint(); } function on_mouse_lbtn_up(x, y) { if (dragging) { dragging = false; return; } // koniec przeciagania paska for (var i = 0; i < buttons.length; i++) { var b = buttons[i]; if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) { doAction(b.action); return; } } var idx = hitItem(x, y); if (idx < 0) return; if (screen === "library") { selectedBook = idx; screen = "book"; scroll = 0; repaint(); } else { var book = books[selectedBook]; var pr = state.progress[book.path] || {}; var startPos = (pr.chapter === idx && pr.position) ? pr.position : 0; playBook(book, idx, startPos); } } function hitItem(x, y) { var top = TOOLBAR_H; if (y < top) return -1; if (screen === "library") { if (!books.length) return -1; var view = state.settings.view; if (view === 0 || view === 1) { var rowH = (view === 1) ? DETAIL_H : ROW_H; var idx = Math.floor((y - top + scroll) / rowH); return (idx >= 0 && idx < books.length) ? idx : -1; } else { var big = (view === 3); var pad = 12, cell = big ? 150 : 92, labelH = big ? 38 : 28; var cols = Math.max(1, Math.floor((ww - pad - SB_W) / (cell + pad))); var rowH = cell + labelH + pad; var startX = Math.floor((ww - SB_W - cols * (cell + pad) + pad) / 2); var row = Math.floor((y - top - pad + scroll) / rowH); var col = Math.floor((x - startX) / (cell + pad)); if (col < 0 || col >= cols) return -1; var idx2 = row * cols + col; return (idx2 >= 0 && idx2 < books.length) ? idx2 : -1; } } else { var book = books[selectedBook]; if (!book) return -1; var coverSize = Math.min(180, Math.floor(ww / 3)); var listTop = top + 16 + coverSize + 16; if (y < listTop) return -1; var idx3 = Math.floor((y - listTop + scroll) / ROW_H); return (idx3 >= 0 && idx3 < book.chapters.length) ? idx3 : -1; } } function on_mouse_wheel(step) { scroll -= step * 60; if (scroll < 0) scroll = 0; if (scroll > maxScroll) scroll = maxScroll; repaint(); } function doAction(a) { switch (a) { case "importFiles": importFiles(); break; case "importSingle": importSingle(); break; case "importDir": importDir(); break; case "view0": state.settings.view = 0; scroll = 0; saveState(); repaint(); break; case "view1": state.settings.view = 1; scroll = 0; saveState(); repaint(); break; case "view2": state.settings.view = 2; scroll = 0; saveState(); repaint(); break; case "view3": state.settings.view = 3; scroll = 0; saveState(); repaint(); break; case "theme": state.settings.theme = (state.settings.theme + 1) % THEMES.length; saveState(); repaint(); break; case "lang": state.settings.lang = (L() === "en") ? "pl" : "en"; saveState(); repaint(); break; case "back": screen = "library"; scroll = 0; repaint(); break; case "resume": resumeBook(books[selectedBook]); break; case "fromStart": playBook(books[selectedBook], 0, 0); break; } } function on_mouse_rbtn_up(x, y) { try { var m = window.CreatePopupMenu(); m.AppendMenuItem(0, 1, tr("mRefresh")); m.AppendMenuItem(0, 2, tr("mSortTitle")); m.AppendMenuItem(0, 3, tr("mSortAuthor")); m.AppendMenuSeparator(); m.AppendMenuItem(0, 4, tr("mClear")); var r = m.TrackPopupMenu(x, y); if (r === 1) { rebuildLibrary(); repaint(); } else if (r === 2) { state.settings.sortBy = "name"; saveState(); rebuildLibrary(); repaint(); } else if (r === 3) { state.settings.sortBy = "author"; saveState(); rebuildLibrary(); repaint(); } else if (r === 4) { state.roots = []; state.customBooks = []; saveState(); rebuildLibrary(); screen = "library"; repaint(); } return true; } catch (e) { trace("rbtn: " + e); return false; } } // ustawia pozycje z kilkoma probami (czasem trzeba poczekac az utwor sie rozkreci) function seekRetry(pos, tries) { try { fb.PlaybackTime = pos; } catch (e) {} var cur = 0; try { cur = fb.PlaybackTime; } catch (e2) {} if (Math.abs(cur - pos) <= 3) { curPos = Math.floor(cur); resuming = false; return; } if (tries > 0) setT(function () { seekRetry(pos, tries - 1); }, 250); else { resuming = false; } // poddajemy sie, ale odblokuj zapis } function on_playback_new_track(handle) { nowPlayingPath = handle ? handle.Path : ""; if (pendingResume && lc(nowPlayingPath) === pendingResume.path && pendingResume.position > 0) { var pos = pendingResume.position; setT(function () { seekRetry(pos, 12); }, 150); } else { resuming = false; } pendingResume = null; repaint(); } function on_playback_time(t) { if (resuming) return; // nie zapisuj, dopoki nie doskoczymy do pozycji curPos = Math.floor(fb.PlaybackTime); // zapamietuj ostatnia DOBRA pozycje saveTick++; saveProgress(false, curPos); if (saveTick % 5 === 0) saveState(); if (screen === "book") repaint(); } function on_playback_pause(p) { if (resuming) return; var pt = 0; try { pt = Math.floor(fb.PlaybackTime); } catch (e) {} if (pt > 0) curPos = pt; saveProgress(true, curPos); } // UWAGA: przy stop foobar zwraca PlaybackTime=0 - zapisujemy OSTATNIA znana pozycje (curPos) function on_playback_stop(reason) { if (resuming) return; saveProgress(true, curPos); } // ==================================================================== // START // ==================================================================== try { on_size(); loadState(); rebuildLibrary(); repaint(); trace("Start OK. Ksiazek: " + books.length + ", roots: " + state.roots.length); } catch (startErr) { initError = (initError ? initError + " | " : "") + "Blad startu: " + startErr; trace("START: " + startErr); repaint(); }