diff --git a/.gitignore b/.gitignore index 3f804b3..4dc56bd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ CodeGarden.egg-info/ htmlcov/ *.db garden-repos/ +todos.json diff --git a/LICENSE.md b/LICENSE.md index 172986d..6c6221f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 C.N. Joseph +Copyright (c) 2024 C.N. Joseph Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/code_garden/config.py b/code_garden/config.py index a20f259..2e0847c 100644 --- a/code_garden/config.py +++ b/code_garden/config.py @@ -13,10 +13,7 @@ def get(): - return { - "HOME_DIR": str(HOME_DIR), - "PORT": PORT, - } + return dotenv.dotenv_values() def set(key, value): diff --git a/code_garden/database.py b/code_garden/database.py new file mode 100644 index 0000000..7ed5966 --- /dev/null +++ b/code_garden/database.py @@ -0,0 +1,23 @@ +import sqlite3 + +from code_garden import config + + +class Connector: + def __init__(self): + self.db = sqlite3.connect(config.HOME_DIR / "todos.db", check_same_thread=False) + + def write(self, stmt: str, params=()): + with self.db as db: + cursor = db.cursor() + cursor.execute(stmt, params) + + def read(self, stmt: str, params=()): + results = None + with self.db as db: + cursor = db.cursor() + cursor.execute(stmt, params) + + results = cursor.fetchall() + + return results diff --git a/code_garden/models.py b/code_garden/models.py index a850848..13a6fd1 100644 --- a/code_garden/models.py +++ b/code_garden/models.py @@ -154,7 +154,7 @@ def init(self, brief_descrip: str): Args: brief_descrip (str): Short description of what the Repository contains. """ - files = ["LICENSE.md", ".gitignore"] + files = ["LICENSE.md", ".gitignore", "todos.json"] self.path.mkdir() Readme(self.name, brief_descrip).write(self.path) for i in files: @@ -211,6 +211,25 @@ def export(self): with open(config.HOME_DIR / f"{self.name}.json", "w") as f: json.dump(self.to_dict(), f, indent=4) + def export_todos(self): + """Export all todos to single JSON file.""" + results = {"todos": [i.to_dict() for i in self.todos]} + json.dump(results, open(self.path / "todos.json", "w"), indent=4) + + def import_todos(self): + """Import todos from a JSON file.""" + results = json.load(open(self.path / "todos.json")).get("todos") + for i in results: + todo_ = Todo( + i.get("name"), + i.get("description"), + i.get("tag"), + datetime.datetime.now(), + i.get("status"), + self.name, + ) + todo_.add() + def to_dict(self): """Get a dict representation of the Repository object (for API usage).""" return dict( @@ -298,6 +317,16 @@ def __init__(self, repository, name, timestamp, abbrev_hash): self.timestamp = timestamp self.abbrev_hash = abbrev_hash + @classmethod + def get(cls, repository, abbrev_hash): + """Get more details about a specified commit.""" + info = ( + Repository(repository) + .run_command(["git", "show", "--stat", abbrev_hash]) + .splitlines()[4:] + ) + return "\n".join(info) + def to_dict(self): """Get a dict representation of this object (for API use).""" return dict( @@ -327,8 +356,14 @@ def path(self): @property def color(self): - choices = {"M": "orange", "A": "green", "D": "red", "R": "yellow", "?": "green"} - return choices.get(self.type_) + choices = { + "M": "#bf5408", + "A": "green", + "D": "red", + "R": "yellow", + "?": "green", + } + return choices.get(self.type_) or "green" def reset(self): """Reset this file to its original state in the most recent commit.""" diff --git a/code_garden/todos.py b/code_garden/todos.py index 2d630c6..f0f15d3 100644 --- a/code_garden/todos.py +++ b/code_garden/todos.py @@ -5,11 +5,10 @@ import click -from code_garden import config +from code_garden.database import Connector -db = sqlite3.connect(config.HOME_DIR / "todos.db", check_same_thread=False) -cursor = db.cursor() -cursor.execute( +conn = Connector() +conn.write( "CREATE TABLE IF NOT EXISTS todos (title TEXT, description TEXT, tag TEXT, date_added DATETIME, status TEXT, repo TEXT, id INTEGER PRIMARY KEY AUTOINCREMENT)" ) @@ -36,7 +35,7 @@ def __init__( self.id = id def add(self): - cursor.execute( + conn.write( "INSERT INTO todos (title, description, tag, date_added, status, repo) VALUES (?,?,?,?,?,?)", ( self.title, @@ -47,41 +46,36 @@ def add(self): self.repo, ), ) - db.commit() @classmethod def get(cls, id): - cursor.execute( + result = conn.read( "SELECT title, description, tag, date_added, status, repo, id FROM todos WHERE id=?", (str(id),), - ) - result = cursor.fetchone() + )[0] return Todo( result[0], result[1], result[2], result[3], result[4], result[5], result[6] ) @classmethod def see_list(cls, repo): - cursor.execute( + results = conn.read( "SELECT title, description, tag, date_added, status, repo, id FROM todos WHERE repo=?", (repo,), ) - results = cursor.fetchall() return sorted( [Todo(i[0], i[1], i[2], i[3], i[4], i[5], i[6]) for i in results], key=lambda x: (x.status == "completed", x.status != "active", x.id), ) def edit(self): - cursor.execute( + conn.write( "UPDATE todos SET title=?, description=?, tag=?, status=? WHERE id=?", (self.title, self.description, self.tag, self.status, str(self.id)), ) - db.commit() def delete(self): - cursor.execute("DELETE FROM todos WHERE id=?", (str(self.id),)) - db.commit() + conn.write("DELETE FROM todos WHERE id=?", (str(self.id),)) def __str__(self): return "#{} {:40.40} ({})".format( diff --git a/code_garden/web/routes.py b/code_garden/web/routes.py index 3412bd0..c8cc74e 100644 --- a/code_garden/web/routes.py +++ b/code_garden/web/routes.py @@ -5,7 +5,7 @@ from code_garden.todos import Todo from .. import config -from ..models import Branch, DiffItem, IgnoreItem, Repository +from ..models import Branch, DiffItem, IgnoreItem, LogItem, Repository @current_app.get("/") @@ -15,10 +15,7 @@ def index(): @current_app.post("/settings") def settings(): - config_ = config.get() - config_.update({"debug": current_app.config.get("ENV") == "development"}) - - return config_ + return config.get() @current_app.post("/repositories") @@ -34,6 +31,15 @@ def commit(): return {"status": "done"} +@current_app.post("/get_commit") +def get_commit(): + return { + "details": LogItem.get( + request.json.get("name"), request.json.get("abbrev_hash") + ) + } + + @current_app.post("/create_repository") def create_repository(): repository_ = Repository(request.json.get("name")) @@ -171,6 +177,20 @@ def toggle_todo(): return {"status": "done"} +@current_app.post("/export_todos") +def export_todos(): + Repository(request.json.get("name")).export_todos() + + return {"status": "done"} + + +@current_app.post("/import_todos") +def import_todos(): + Repository(request.json.get("name")).import_todos() + + return {"status": "done"} + + @current_app.post("/commit_todo") def commit_todo(): todo_ = Todo.get(request.json.get("id")) diff --git a/code_garden/web/static/App.css b/code_garden/web/static/App.css index dcbeed0..9ce3b8f 100644 --- a/code_garden/web/static/App.css +++ b/code_garden/web/static/App.css @@ -81,7 +81,7 @@ body { .dropdown-menu { background-color: var(--primary-bg); color: var(--primary-txt); - border: 1px solid var(--primary-txt); + border: 1px solid var(--btn-color); } .dropdown-item { @@ -90,6 +90,11 @@ body { letter-spacing: 1px; } +.dropdown-item:hover { + background-color: var(--btn-color); + color: var(--btn-hover); +} + a, a:hover { color: inherit; @@ -173,3 +178,8 @@ a:hover { .desc:focus { outline: none !important; } + +.diff-item:hover, +.log-item:hover { + border-bottom: 1px dotted; +} diff --git a/code_garden/web/static/App.jsx b/code_garden/web/static/App.jsx index 7c146ac..47bf975 100644 --- a/code_garden/web/static/App.jsx +++ b/code_garden/web/static/App.jsx @@ -30,6 +30,7 @@ const ReposContext = React.createContext(); const CurrentRepoContext = React.createContext(); const LoadingContext = React.createContext(); const TabContext = React.createContext(); +const PageContext = React.createContext(); const tags = [ "misc", @@ -92,6 +93,78 @@ function BranchForm() { ); } +function CreateRepoForm() { + const [, setLoading] = React.useContext(LoadingContext); + const [, , getRepo] = React.useContext(CurrentRepoContext); + const [, , getRepos] = React.useContext(ReposContext); + const [name, setName] = React.useState(""); + const [description, setDescription] = React.useState(""); + const [page, setPage] = React.useContext(PageContext); + + const createRepo = (e) => { + e.preventDefault(); + setLoading(true); + apiCall( + "/create_repository", + { + name: name, + brief_descrip: description, + }, + (data) => { + getRepo(data.name); + setLoading(false); + setName(""); + setDescription(""); + setPage("repos"); + getRepos(); + } + ); + }; + + const onChangeName = (e) => { + setName(e.target.value); + }; + + const onChangeDescription = (e) => { + setDescription(e.target.value); + }; + + const generateName = () => { + apiCall("/generate_name", {}, (data) => { + setName(data.name); + setDescription(`Created on ${new Date().toDateString()}`); + }); + }; + + return ( +
createRepo(e)}> +
+ + generateName()} className="btn"> + Generate Name + +
+ + +
+ ); +} + function BranchItem({ item }) { const [currentRepo, setCurrentRepo, getRepo] = React.useContext(CurrentRepoContext); @@ -148,22 +221,34 @@ function BranchItem({ item }) {
{item.name}
- - - {deleting && ( - + {!["master", "main"].includes(item.name) && ( + <> + {deleting && ( + + )} + + )} -
); @@ -209,7 +294,7 @@ function Branches() { {showAll && ( @@ -228,10 +313,10 @@ function Branches() { )}

@@ -272,6 +357,7 @@ function TodoForm() { onSubmit={(e) => createTodo(e)}> - + {tag}
@@ -308,7 +394,7 @@ function TodoForm() { function CommandForm() { const [, setLoading] = React.useContext(LoadingContext); - const [currentRepo, ,] = React.useContext(CurrentRepoContext); + const [currentRepo, , getRepo] = React.useContext(CurrentRepoContext); const [cmd, setCmd] = React.useState(""); const onChangeCmd = (e) => setCmd(e.target.value); @@ -324,6 +410,7 @@ function CommandForm() { }, function (data) { setCmd(""); + getRepo(currentRepo.name); setLoading(false); } ); @@ -408,8 +495,11 @@ function DiffItem({ item }) { }; return ( -
-
{item.name}
+
+ + + {item.name} +
{deleting && (
setShowDescription(!showDescription)}>
- {showDescription && ( + {(showDescription || item.status === "active") && ( + className="form-control h-100 border-0"> )}
@@ -811,9 +993,33 @@ function Changes() { function Log() { const [currentRepo, setCurrentRepo, getRepo] = React.useContext(CurrentRepoContext); + const [loading, setLoading] = React.useContext(LoadingContext); + + const revertCommit = () => { + setLoading(true); + apiCall( + "/run_command", + { + repository: currentRepo.name, + cmd: "git reset --soft HEAD~1", + }, + function (data) { + getRepo(currentRepo.name); + setLoading(false); + } + ); + }; return (
+ {currentRepo.log.length > 0 && ( + + )} {currentRepo.log.map((x, id) => ( ))} @@ -840,8 +1046,46 @@ function Todos() { ); }; + const exportTodos = () => { + setLoading(true); + apiCall( + "/export_todos", + { + name: currentRepo.name, + }, + function (data) { + getRepo(currentRepo.name); + setLoading(false); + } + ); + }; + + const importTodos = () => { + setLoading(true); + apiCall( + "/import_todos", + { + name: currentRepo.name, + }, + function (data) { + getRepo(currentRepo.name); + setLoading(false); + } + ); + }; + return (
+
+ + +
@@ -871,8 +1115,10 @@ function Todos() { ))} {currentRepo.todos.filter((x) => x.done).length !== 0 && (
-
)} @@ -888,8 +1134,9 @@ function Ignored() { return (
+ {currentRepo.ignored.map((x, id) => ( - + ))}
); @@ -934,46 +1181,57 @@ function SideNav() { {currentRepo.length !== 0 && (
-
+
@@ -992,6 +1250,7 @@ function TopNav() { React.useContext(CurrentRepoContext); const [deleting, setDeleting] = React.useState(false); const [copied, setCopied] = React.useState(false); + const [page, setPage] = React.useContext(PageContext); React.useEffect(() => { localStorage.setItem("CodeGarden", theme); @@ -1067,23 +1326,33 @@ function TopNav() { {currentRepo.name || "Select Repo"} -
+
+ {repos.map((x) => ( @@ -1094,30 +1363,41 @@ function TopNav() {
- copyPath()}> + copyPath()}> {currentRepo.remote_url && ( )} - exportRepository()}> + exportRepository()}> setDeleting(!deleting)}> - + {deleting && ( deleteRepository()}> - + Delete Repo )} @@ -1147,7 +1427,7 @@ function TopNav() { ))}
- { setLoading(true); @@ -1183,6 +1466,22 @@ function MultiContext(props) { }); }; + const getConfig = () => { + setLoading(true); + apiCall("/settings", {}, (data) => { + setConfig(data); + setLoading(false); + }); + }; + + const copyConfig = () => { + navigator.clipboard.writeText(JSON.stringify(config, null, 4)); + setCopied(true); + setTimeout(function () { + setCopied(false); + }, 1500); + }; + React.useEffect(() => { getRepos(); localStorage.getItem("last-repo-opened") && @@ -1190,39 +1489,63 @@ function MultiContext(props) { }, []); React.useEffect(() => { - currentRepo.length !== 0 && - localStorage.setItem("last-repo-opened", currentRepo.name); + currentRepo.length !== 0 + ? localStorage.setItem("last-repo-opened", currentRepo.name) + : localStorage.removeItem("last-repo-opened"); }, [currentRepo]); + React.useEffect(() => { + if (page !== "repos") { + setCurrentRepo([]); + page === "settings" ? getConfig() : setConfig([]); + } + }, [page]); + return ( <> - - - - {props.children} - - - + + + + +
+ + {page === "repos" ? ( + <> +
+ + +
+ + ) : page === "settings" ? ( +
+
+ {JSON.stringify(config, null, 4)} +
+ +
+ ) : ( +
+ +
+ )} +
+
+
+
+
); } -function App() { - return ( - -
- -
- - -
-
-
- ); -} - const root = ReactDOM.createRoot(document.getElementById("root")); root.render(); diff --git a/docs/screenshot1.png b/docs/screenshot1.png index 354262d..8e5e85f 100644 Binary files a/docs/screenshot1.png and b/docs/screenshot1.png differ diff --git a/docs/screenshot2.png b/docs/screenshot2.png index d007d48..706cdf3 100644 Binary files a/docs/screenshot2.png and b/docs/screenshot2.png differ diff --git a/setup.py b/setup.py index acf7dfe..5598a86 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="CodeGarden", - version="2023.09.06", + version="2024.01.04b", long_description=open("README.md").read(), license=open("LICENSE.md").read(), entry_points={"console_scripts": ["garden=code_garden.__main__:main"]},