diff --git a/Source/BibleVerse.spoon/config.lua b/Source/BibleVerse.spoon/config.lua new file mode 100644 index 00000000..f67e54bf --- /dev/null +++ b/Source/BibleVerse.spoon/config.lua @@ -0,0 +1,10 @@ +--- Default configuration for BibleVerse Spoon. +return { + translation = "UBIO", + refresh_interval = 3600, + background = { color = { red = 0.1, green = 0.1, blue = 0.15 }, alpha = 0.9, corner_radius = 12 }, + font = { name = nil, size = 14, color = { white = 0.95 }, reference_size = 12, reference_color = { red = 0.6, green = 0.7, blue = 0.9 } }, + width = 400, + height = 165, + position = { default = { x = -410, y = -175 } } +} diff --git a/Source/BibleVerse.spoon/docs.json b/Source/BibleVerse.spoon/docs.json new file mode 100644 index 00000000..53bb2dd8 --- /dev/null +++ b/Source/BibleVerse.spoon/docs.json @@ -0,0 +1,7 @@ +{ + "name": "BibleVerse", + "version": "2.0", + "description": "Desktop widget displaying random New Testament verses in Ukrainian or English", + "author": "Oleksandr", + "license": "MIT" +} diff --git a/Source/BibleVerse.spoon/init.lua b/Source/BibleVerse.spoon/init.lua new file mode 100644 index 00000000..539b2371 --- /dev/null +++ b/Source/BibleVerse.spoon/init.lua @@ -0,0 +1,47 @@ +--- BibleVerse Spoon - displays random Bible verses as desktop widget. +local obj = {} +obj.__index = obj +obj.name = "BibleVerse" +obj.version = "2.0" +obj.author = "Oleksandr" +obj.license = "MIT - https://opensource.org/licenses/MIT" +obj.homepage = "https://github.com/auraz/BibleVerseSpoon" + +local verse = dofile(hs.spoons.resourcePath("verse.lua")) +local widget = dofile(hs.spoons.resourcePath("widget.lua")) +local default_config = dofile(hs.spoons.resourcePath("config.lua")) + +obj.config = {} +for k, v in pairs(default_config) do obj.config[k] = v end + +local state = { canvas = nil, timer = nil, watcher = nil } + +function obj:refresh() + verse.fetch(self.config.translation, function(data) + if not data then return end + local text = verse.clean_text(data.text) + local ref = verse.format_reference(data, self.config.translation) + state.canvas = widget.render(state, text, ref, self.config, data) + end) + return self +end + +function obj:start() + self:refresh() + state.timer = hs.timer.doEvery(self.config.refresh_interval, function() self:refresh() end) + state.watcher = hs.caffeinate.watcher.new(function(e) + if e == hs.caffeinate.watcher.systemDidWake then self:refresh() end + end) + state.watcher:start() + return self +end + +function obj:stop() + if state.timer then state.timer:stop() end + if state.watcher then state.watcher:stop() end + widget.destroy(state.canvas) + state = { canvas = nil, timer = nil, watcher = nil } + return self +end + +return obj diff --git a/Source/BibleVerse.spoon/translations.lua b/Source/BibleVerse.spoon/translations.lua new file mode 100644 index 00000000..970e4c77 --- /dev/null +++ b/Source/BibleVerse.spoon/translations.lua @@ -0,0 +1,19 @@ +--- Book name translations for New Testament (books 40-66). +return { + UBIO = { + [40] = "Матвія", [41] = "Марка", [42] = "Луки", [43] = "Івана", [44] = "Дії", + [45] = "Римлян", [46] = "1 Коринтян", [47] = "2 Коринтян", [48] = "Галатів", [49] = "Ефесян", + [50] = "Филип'ян", [51] = "Колосян", [52] = "1 Солунян", [53] = "2 Солунян", [54] = "1 Тимофія", + [55] = "2 Тимофія", [56] = "Тита", [57] = "Филимона", [58] = "Євреїв", [59] = "Якова", + [60] = "1 Петра", [61] = "2 Петра", [62] = "1 Івана", [63] = "2 Івана", [64] = "3 Івана", + [65] = "Юди", [66] = "Об'явлення" + }, + KJV = { + [40] = "Matthew", [41] = "Mark", [42] = "Luke", [43] = "John", [44] = "Acts", + [45] = "Romans", [46] = "1 Corinthians", [47] = "2 Corinthians", [48] = "Galatians", [49] = "Ephesians", + [50] = "Philippians", [51] = "Colossians", [52] = "1 Thessalonians", [53] = "2 Thessalonians", [54] = "1 Timothy", + [55] = "2 Timothy", [56] = "Titus", [57] = "Philemon", [58] = "Hebrews", [59] = "James", + [60] = "1 Peter", [61] = "2 Peter", [62] = "1 John", [63] = "2 John", [64] = "3 John", + [65] = "Jude", [66] = "Revelation" + } +} diff --git a/Source/BibleVerse.spoon/verse.lua b/Source/BibleVerse.spoon/verse.lua new file mode 100644 index 00000000..a2ea652f --- /dev/null +++ b/Source/BibleVerse.spoon/verse.lua @@ -0,0 +1,32 @@ +--- Verse fetching and parsing functions. +local translations = dofile(hs.spoons.resourcePath("translations.lua")) + +local M = {} +local API_BASE = "https://bolls.life/get-random-verse/" + +function M.clean_text(raw) + return raw:gsub("<[^>]+>", ""):gsub(" ", " "):gsub("%s+", " "):match("^%s*(.-)%s*$") +end + +function M.get_book_name(book_num, translation) + local books = translations[translation] + return books and books[book_num] or ("Book " .. book_num) +end + +function M.format_reference(data, translation) + return M.get_book_name(data.book, translation) .. " " .. data.chapter .. ":" .. data.verse +end + +function M.fetch(translation, callback) + local function try_fetch() + hs.http.asyncGet(API_BASE .. translation .. "/", nil, function(status, body) + if status ~= 200 then callback(nil) return end + local data = hs.json.decode(body) + if data.book < 40 or data.book > 66 then try_fetch() return end + callback(data) + end) + end + try_fetch() +end + +return M diff --git a/Source/BibleVerse.spoon/widget.lua b/Source/BibleVerse.spoon/widget.lua new file mode 100644 index 00000000..fd50de14 --- /dev/null +++ b/Source/BibleVerse.spoon/widget.lua @@ -0,0 +1,46 @@ +--- Widget rendering functions. +local M = {} + +function M.calculate_position(screen_frame, config, screen_name) + local pos = config.position[screen_name] or config.position.default + local x = pos.x >= 0 and pos.x or (screen_frame.w + pos.x - config.width) + local y = pos.y >= 0 and pos.y or (screen_frame.h + pos.y - config.height) + return { x = screen_frame.x + x, y = screen_frame.y + y } +end + +function M.create_elements(text, reference, config) + local bg = config.background + local font = config.font + return { + { id = "bg", type = "rectangle", action = "fill", trackMouseUp = true, roundedRectRadii = { xRadius = bg.corner_radius, yRadius = bg.corner_radius }, fillColor = { red = bg.color.red, green = bg.color.green, blue = bg.color.blue, alpha = bg.alpha } }, + { type = "rectangle", action = "stroke", roundedRectRadii = { xRadius = bg.corner_radius, yRadius = bg.corner_radius }, strokeColor = { red = 0.3, green = 0.4, blue = 0.5, alpha = 0.5 }, strokeWidth = 1 }, + { type = "text", text = text, textColor = font.color, textSize = font.size, textFont = font.name, textAlignment = "left", frame = { x = "5%", y = "8%", w = "90%", h = "68%" } }, + { type = "text", text = "— " .. reference, textColor = font.reference_color, textSize = font.reference_size, textFont = font.name, textAlignment = "right", frame = { x = "5%", y = "78%", w = "90%", h = "18%" } } + } +end + +function M.render(state, text, reference, config, verse_data) + if state.canvas then state.canvas:delete() end + local screen = hs.screen.mainScreen() + local frame = screen:frame() + local pos = M.calculate_position(frame, config, screen:name()) + local canvas = hs.canvas.new({ x = pos.x, y = pos.y, w = config.width, h = config.height }) + canvas:appendElements(M.create_elements(text, reference, config)) + canvas:level(hs.canvas.windowLevels.floating) + canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + canvas:mouseCallback(function(c, msg, id, x, y) + if msg == "mouseUp" and verse_data then + local url = "https://bolls.life/" .. verse_data.translation .. "/" .. verse_data.book .. "/" .. verse_data.chapter .. "/" + hs.urlevent.openURL(url) + end + end) + canvas:canvasMouseEvents(false, true, false, false) + canvas:show() + return canvas +end + +function M.destroy(canvas) + if canvas then canvas:delete() end +end + +return M