Repos / pytaku / 61300c25af
commit 61300c25af2d34b68b83e825169acd62854489d6
Author: Bùi Thành Nhân <hi@imnhan.com>
Date: Fri Aug 21 22:52:13 2020 +0700
implement mithril search
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 0791098..29b5d01 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -366,7 +366,9 @@ def ensure_titles(site_title_pairs: List[Tuple[str, str]]):
@app.route("/h")
@app.route("/a")
@app.route("/f")
-def home_view():
+@app.route("/s")
+@app.route("/s/<query>")
+def home_view(query=None):
return render_template("spa.html")
@@ -442,3 +444,15 @@ def api_follows():
thumbnail = url_for("proxy_view", b64_url=_encode_proxy_url(thumbnail))
title["thumbnail"] = thumbnail
return jsonify({"titles": titles})
+
+
+@app.route("/api/search/<query>", methods=["GET"])
+def api_search(query):
+ results = search_title_all_sites(query)
+
+ if "mangadex" in results:
+ for title in results["mangadex"]:
+ title["thumbnail"] = url_for(
+ "proxy_view", b64_url=_encode_proxy_url(title["thumbnail"])
+ )
+ return results
diff --git a/src/pytaku/static/js/layout.js b/src/pytaku/static/js/layout.js
index 7807b0f..7698c14 100644
--- a/src/pytaku/static/js/layout.js
+++ b/src/pytaku/static/js/layout.js
@@ -1,7 +1,8 @@
-import { Auth } from "./models.js";
+import { Auth, SearchModel } from "./models.js";
function Navbar(initialVNode) {
let isLoggingOut = false;
+
return {
view: (vnode) => {
let userLink;
@@ -50,10 +51,24 @@ function Navbar(initialVNode) {
}),
]
),
- m("form.nav--search-form", [
- m("input", { placeholder: "search title name" }),
- m("button", { type: "submit" }, [m("i.icon.icon-search")]),
- ]),
+ m(
+ "form.nav--search-form",
+ {
+ onsubmit: (ev) => {
+ ev.preventDefault();
+ m.route.set("/s/:query", { query: SearchModel.query });
+ },
+ },
+ [
+ m("input[placeholder=search manga title]", {
+ onchange: (ev) => {
+ SearchModel.query = ev.target.value;
+ },
+ value: SearchModel.query,
+ }),
+ m("button[type=submit]", [m("i.icon.icon-search")]),
+ ]
+ ),
userLink,
]);
},
diff --git a/src/pytaku/static/js/main.js b/src/pytaku/static/js/main.js
index 2dde99a..7863e87 100644
--- a/src/pytaku/static/js/main.js
+++ b/src/pytaku/static/js/main.js
@@ -3,6 +3,7 @@ import Layout from "./layout.js";
import Authentication from "./routes/authentication.js";
import Home from "./routes/home.js";
import Follows from "./routes/follows.js";
+import Search from "./routes/search.js";
Auth.init().then(() => {
const root = document.getElementById("spa-root");
@@ -16,7 +17,7 @@ Auth.init().then(() => {
return Home;
}
},
- render: () => m(Layout, m(Home)),
+ render: (vnode) => m(Layout, vnode),
},
"/a": {
onmatch: () => {
@@ -26,18 +27,30 @@ Auth.init().then(() => {
return Authentication;
}
},
- render: () => m(Layout, m(Authentication)),
+ render: (vnode) => m(Layout, vnode),
},
"/f": {
onmatch: () => {
if (Auth.isLoggedIn()) {
return Follows;
} else {
- //m.route.set("/a", null, { replace: true });
- return m("h1", "waiting");
+ m.route.set("/a", null, { replace: true });
}
},
- render: () => m(Layout, m(Follows)),
+ render: (vnode) => m(Layout, vnode),
+ },
+ "/s/:query": {
+ render: (vnode) =>
+ m(
+ Layout,
+ m(Search, {
+ query: vnode.attrs.query,
+ key: vnode.attrs.query,
+ // ^ set a key here to reinitialize Search component on route
+ // change. Without it, Search.oninit would only trigger once on
+ // first full page load.
+ })
+ ),
},
});
});
diff --git a/src/pytaku/static/js/models.js b/src/pytaku/static/js/models.js
index e1dc2b4..0159684 100644
--- a/src/pytaku/static/js/models.js
+++ b/src/pytaku/static/js/models.js
@@ -89,4 +89,35 @@ const Auth = {
},
};
-export { Auth };
+const SearchModel = {
+ query: "",
+ result: {},
+ cache: {},
+ isLoading: true,
+ performSearch: (query) => {
+ SearchModel.query = query;
+ if (SearchModel.cache[query]) {
+ SearchModel.result = SearchModel.cache[query];
+ } else {
+ SearchModel.isLoading = true;
+ m.redraw();
+ m.request({
+ method: "GET",
+ url: "/api/search/:query",
+ params: { query },
+ })
+ .then((resp) => {
+ SearchModel.cache[query] = resp;
+ SearchModel.result = resp;
+ })
+ .catch((err) => {
+ console.log("TODO", err);
+ })
+ .finally(() => {
+ SearchModel.isLoading = false;
+ });
+ }
+ },
+};
+
+export { Auth, SearchModel };
diff --git a/src/pytaku/static/js/routes/follows.js b/src/pytaku/static/js/routes/follows.js
index 06679fc..7097eec 100644
--- a/src/pytaku/static/js/routes/follows.js
+++ b/src/pytaku/static/js/routes/follows.js
@@ -1,7 +1,5 @@
import { Auth } from "../models.js";
-
-const truncate = (input) =>
- input.length > 20 ? `${input.substring(0, 20)}...` : input;
+import { LoadingMessage, truncate } from "../utils.js";
function fullChapterName(chapter) {
let result = "Chapter " + chapter.num_major;
@@ -51,7 +49,7 @@ const Title = {
[
fullChapterName(chapter),
chapter.groups.map((group) => {
- m("span.follows--group", truncate(group));
+ m("span.follows--group", truncate(group, 20));
}),
]
)
@@ -93,10 +91,7 @@ function Follows(initialVNode) {
let content = "";
if (isLoading) {
- return m(
- "div.content",
- m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."])
- );
+ return m("div.content", m(LoadingMessage));
}
if (titles.length === 0) {
diff --git a/src/pytaku/static/js/routes/search.js b/src/pytaku/static/js/routes/search.js
new file mode 100644
index 0000000..6551504
--- /dev/null
+++ b/src/pytaku/static/js/routes/search.js
@@ -0,0 +1,51 @@
+import { Auth, SearchModel } from "../models.js";
+import { LoadingMessage, truncate } from "../utils.js";
+
+const Search = {
+ oninit: (vnode) => {
+ document.title = `"${vnode.attrs.query}" search results`;
+ SearchModel.performSearch(vnode.attrs.query);
+ },
+ view: (vnode) => {
+ return m(
+ "div.content",
+ SearchModel.isLoading
+ ? m(LoadingMessage)
+ : Object.entries(SearchModel.result).map(([site, titles]) =>
+ m("div", [
+ m("h1.search--site-heading", site),
+ titles
+ ? m("p.search--result-text", [
+ "Showing ",
+ m("strong", titles.length),
+ ` result${titles.length > 1 ? "s" : ""} for `,
+ SearchModel.query,
+ ])
+ : m(
+ "p.search--result-text",
+ `No results for "${SearchModel.query}"`
+ ),
+ m(
+ "div.search--results",
+ titles.map((title) =>
+ m(
+ m.route.Link,
+ {
+ class: "search--result",
+ href: `/m/${site}/${title.id}`,
+ title: title.name,
+ },
+ [
+ m("img", { src: title.thumbnail, alt: title.name }),
+ m("span", truncate(title.name, 50)),
+ ]
+ )
+ )
+ ),
+ ])
+ )
+ );
+ },
+};
+
+export default Search;
diff --git a/src/pytaku/static/js/utils.js b/src/pytaku/static/js/utils.js
new file mode 100644
index 0000000..bf0105a
--- /dev/null
+++ b/src/pytaku/static/js/utils.js
@@ -0,0 +1,8 @@
+const LoadingMessage = {
+ view: (vnode) => m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."]),
+};
+
+const truncate = (input, size) =>
+ input.length > size ? `${input.substring(0, size)}...` : input;
+
+export { LoadingMessage, truncate };
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index 4d9eec0..a1db482 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -220,3 +220,32 @@ .follows--more {
display: inline-block;
font-style: italic;
}
+
+/* Search route */
+.search--site-heading {
+ text-transform: capitalize;
+}
+.search--results {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+.search--result {
+ display: inline-flex;
+ flex-direction: column;
+ border: 1px solid var(--bg-black);
+ margin: 0 1rem 1rem 0;
+ background-color: var(--bg-black);
+ color: white;
+ text-decoration: none;
+ max-width: 150px;
+}
+.search--result span {
+ padding: 0.5rem;
+ width: 0;
+ min-width: 100%;
+}
+
+.search--result-text {
+ margin-bottom: 1rem;
+}