diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua index 8e0cfcd..589d1ca 100644 --- a/lua/leetcode-ui/group/page/problems.lua +++ b/lua/leetcode-ui/group/page/problems.lua @@ -21,6 +21,12 @@ local list = Button("List", { on_press = cmd.problems, }) +local recent = Button("Recent", { + icon = "", + sc = "i", + on_press = cmd.recent, +}) + local random = Button("Random", { icon = "", sc = "r", @@ -37,6 +43,7 @@ local back = BackButton("menu") page:insert(Buttons({ list, + recent, random, daily, back, diff --git a/lua/leetcode-ui/popup/console/result.lua b/lua/leetcode-ui/popup/console/result.lua index 520113d..724ad4f 100644 --- a/lua/leetcode-ui/popup/console/result.lua +++ b/lua/leetcode-ui/popup/console/result.lua @@ -28,6 +28,9 @@ function ResultPopup:handle(item) end end + local Recent = require("leetcode.cache.recent") + Recent.add(self.console.question.q.title_slug) + self:draw() end diff --git a/lua/leetcode/cache/problemlist.lua b/lua/leetcode/cache/problemlist.lua index 13cbf0d..0322978 100644 --- a/lua/leetcode/cache/problemlist.lua +++ b/lua/leetcode/cache/problemlist.lua @@ -32,6 +32,15 @@ function Problemlist.get() return Problemlist.read().data end +---@return table<string, lc.cache.Question> +function Problemlist.get_map() + local map = {} + for _, q in ipairs(Problemlist.get()) do + map[q.title_slug] = q + end + return map +end + ---@return lc.cache.payload function Problemlist.read() if not file:exists() then diff --git a/lua/leetcode/cache/recent.lua b/lua/leetcode/cache/recent.lua new file mode 100644 index 0000000..e51ee0c --- /dev/null +++ b/lua/leetcode/cache/recent.lua @@ -0,0 +1,91 @@ +local Object = require("nui.object") +local config = require("leetcode.config") +local log = require("leetcode.logger") + +---@alias lc.recent table<string, integer> + +---@type Path +local file = config.storage.cache:joinpath(("recent%s"):format(config.is_cn and "_cn" or "")) + +---@class lc.Recent +local Recent = Object("LeetRecent") + +function Recent.populate() + Recent.write({}) +end + +---@param recent lc.recent|string +function Recent.write(recent) + file:write("return " .. vim.inspect(recent), "w") +end + +---@return lc.recent +function Recent.get() + if not file:exists() then + Recent.populate() + end + + local content = file:read() + local chunk = load(content) + return chunk() +end + +---@return { slug: string, time: integer }[] +function Recent.sorted() + local recent = Recent.get() + + local sorted = {} + for slug, time in pairs(recent) do + table.insert(sorted, { slug = slug, time = time }) + end + + table.sort(sorted, function(a, b) + return a.time > b.time + end) + + return sorted +end + +function Recent.convert() + local map = require("leetcode.cache.problemlist").get_map() + local sorted = Recent.sorted() + + local recent = {} + for _, item in ipairs(sorted) do + if map[item.slug] then + table.insert(recent, map[item.slug]) + end + end + return recent +end + +---@param slug string +---@param recent? lc.recent +function Recent.contains(slug, recent) + if not recent then + recent = Recent.get() + end + + return recent[slug] == true +end + +function Recent.add(slug) + local recent = Recent.get() + + recent[slug] = os.time() + Recent.write(recent) + + return true +end + +function Recent.remove(slug) + local recent = Recent.get() + + local tmp = recent[slug] + recent[slug] = nil + + Recent.write(recent) + return tmp +end + +return Recent diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 1472b51..8ec3101 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -29,6 +29,13 @@ function cmd.problems(options) require("leetcode.pickers.question").pick(p, options) end +function cmd.recent() + require("leetcode.utils").auth_guard() + + -- local p = require("leetcode.cache.problemlist").get() + require("leetcode.pickers.recent").pick() +end + ---@param cb? function function cmd.cookie_prompt(cb) local cookie = require("leetcode.cache.cookie") @@ -631,6 +638,7 @@ cmd.commands = { }, update = { cmd.update_sessions }, }, + recent = { cmd.recent }, list = { cmd.problems, _args = arguments.list, diff --git a/lua/leetcode/pickers/recent.lua b/lua/leetcode/pickers/recent.lua new file mode 100644 index 0000000..2585572 --- /dev/null +++ b/lua/leetcode/pickers/recent.lua @@ -0,0 +1,124 @@ +local log = require("leetcode.logger") +local t = require("leetcode.translator") +local utils = require("leetcode.utils") +local ui_utils = require("leetcode-ui.utils") + +local Question = require("leetcode-ui.question") + +local pickers = require("telescope.pickers") +local finders = require("telescope.finders") +local conf = require("telescope.config").values +local config = require("leetcode.config") + +local entry_display = require("telescope.pickers.entry_display") +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") + +---@param question lc.cache.Question +--- +---@return string +local function question_formatter(question) + return ("%s. %s %s %s"):format( + tostring(question.frontend_id), + question.title, + question.title_cn, + question.title_slug + ) +end + +---@param question lc.cache.Question +local function display_difficulty(question) + local hl = ui_utils.diff_to_hl(question.difficulty) + return { config.icons.square, hl } +end + +---@param question lc.cache.Question +local function display_user_status(question) + if question.paid_only then + return config.auth.is_premium and config.icons.hl.unlock or config.icons.hl.lock + end + + if question.status == vim.NIL then + return { "" } + end + return config.icons.hl.status[question.status] or { "" } +end + +---@param question lc.cache.Question +local function display_question(question) + local index = { question.frontend_id .. ".", "leetcode_normal" } + local title = { utils.translate(question.title, question.title_cn) } + return unpack({ index, title }) +end + +local displayer = entry_display.create({ + separator = " ", + items = { + { width = 1 }, + { width = 1 }, + { width = 5 }, + { remaining = true }, + }, +}) + +local function make_display(entry) + ---@type lc.cache.Question + local q = entry.value + + return displayer({ + display_user_status(q), + display_difficulty(q), + display_question(q), + }) +end + +local function entry_maker(entry) + return { + value = entry, + display = make_display, + ordinal = question_formatter(entry), + } +end + +local theme = require("telescope.themes").get_dropdown({ + layout_config = { + width = 100, + height = 20, + }, +}) + +return { + pick = function() + local Recent = require("leetcode.cache.recent") + local questions = Recent.convert() + + pickers + .new(theme, { + prompt_title = t("Recent Questions"), + finder = finders.new_table({ + results = questions, + entry_maker = entry_maker, + }), + sorter = conf.generic_sorter(theme), + attach_mappings = function(prompt_bufnr, map) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if not selection then + return + end + + local q = selection.value + if q.paid_only and not config.auth.is_premium then + return log.warn("Question is for premium users only") + end + + actions.close(prompt_bufnr) + Question(q):mount() + end) + + return true + end, + }) + :find() + end, +}