Skip to content

Commit

Permalink
Merge pull request #13 from daya0576/henry/order_habits
Browse files Browse the repository at this point in the history
Add reorder page
  • Loading branch information
daya0576 authored Oct 21, 2024
2 parents cee489a + 72c20e1 commit 937eb26
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 256 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ docker run -d --name beaverhabits \
Options:

| Name | Description |
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|:---------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **HABITS_STORAGE**(str) | The `DATABASE` option stores everything in a single SQLite database file named habits.db. On the other hand, the `USER_DISK` option saves habits and records in a local json file. |
| **FIRST_DAY_OF_WEEK**(int) | By default, the first day of the week is set as Monday. To change it to Sunday, you can set it as `6`. |
| **MAX_USER_COUNT**(int) | By setting it to `1`, you can prevent others from signing up in the future. |
| **ENABLE_PWA**(bool) | Experiential feature to support PWA, such as enabling standalone mode on iOS. The default setting is `false` |

# Features

Expand Down
1 change: 1 addition & 0 deletions beaverhabits/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Settings(BaseSettings):

# Customization
FIRST_DAY_OF_WEEK: int = calendar.MONDAY
ENABLE_PWA: bool = False

def is_dev(self):
return self.ENV == "dev"
Expand Down
6 changes: 2 additions & 4 deletions beaverhabits/frontend/add_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@
from beaverhabits.frontend.layout import layout
from beaverhabits.storage.storage import HabitList

grid_classes = "w-full gap-0 items-center"


@ui.refreshable
def add_ui(habit_list: HabitList):
for item in habit_list.habits:
with ui.grid(columns=9, rows=1).classes("w-full gap-0 items-center"):
with ui.grid(columns=8, rows=1).classes("w-full gap-0 items-center"):
name = HabitNameInput(item)
name.classes("col-span-7 break-all")
name.classes("col-span-6 break-all")

star = HabitStarCheckbox(item, add_ui.refresh)
star.props("flat fab-mini color=grey")
Expand Down
23 changes: 20 additions & 3 deletions beaverhabits/frontend/components.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import calendar
import datetime
from dataclasses import dataclass
import datetime
from typing import Callable, Optional

from nicegui import events, ui
Expand Down Expand Up @@ -70,17 +70,34 @@ async def _async_task(self, e: events.ValueChangeEventArguments):
logger.info(f"Day {self.day} ticked: {e.value}")


class HabitOrderCard(ui.card):
def __init__(self, habit: Habit | None = None) -> None:
super().__init__()
self.habit = habit
self.props("flat dense")
self.classes("py-0.5 w-full")
if habit:
self.props("draggable")
self.classes("cursor-grab")


class HabitNameInput(ui.input):
def __init__(self, habit: Habit) -> None:
super().__init__(value=habit.name, on_change=self._async_task)
self.habit = habit
self.validation = lambda value: "Too long" if len(value) > 18 else None
self.props("dense")
self.validation = self._validate
self.props("dense hide-bottom-space")

async def _async_task(self, e: events.ValueChangeEventArguments):
self.habit.name = e.value
logger.info(f"Habit Name changed to {e.value}")

def _validate(self, value: str) -> Optional[str]:
if not value:
return "Name is required"
if len(value) > 18:
return "Too long"


class HabitStarCheckbox(ui.checkbox):
def __init__(self, habit: Habit, refresh: Callable) -> None:
Expand Down
23 changes: 14 additions & 9 deletions beaverhabits/frontend/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
from nicegui import ui

from beaverhabits.app.auth import user_logout
from beaverhabits.configs import settings
from beaverhabits.frontend import icons
from beaverhabits.frontend.components import compat_menu, menu_header, menu_icon_button
from beaverhabits.storage.meta import get_page_title, get_root_path

open_page = ui.navigate.to


def custom_header():
ui.add_head_html(
Expand All @@ -25,29 +28,29 @@ def custom_header():
'<link rel="apple-touch-icon" href="/statics/images/apple-touch-icon-v4.png">'
)

ui.add_head_html('<link rel="manifest" href="/statics/pwa/manifest.json">')
ui.add_head_html(
'<script>if(navigator.standalone === true) { navigator.serviceWorker.register("/statics/pwa/service_worker.js"); };</script>'
)
# ui.add_head_html('<link rel="manifest" href="/statics/pwa/manifest.json">')
# ui.add_head_html(
# '<script>if(navigator.standalone === true) { navigator.serviceWorker.register("/statics/pwa/service_worker.js"); };</script>'
# )


def menu_component(root_path: str) -> None:
"""Dropdown menu for the top-right corner of the page."""
with ui.menu():
compat_menu("Add", lambda: ui.open(os.path.join(root_path, "add")))
compat_menu("Add", lambda: open_page(os.path.join(root_path, "add")))
ui.separator()

compat_menu(
"Export",
lambda: ui.open(os.path.join(root_path, "export"), new_tab=True),
lambda: open_page(os.path.join(root_path, "export"), new_tab=True),
)
ui.separator()

if not root_path.startswith("/demo"):
compat_menu("Import", lambda: ui.open(os.path.join(root_path, "import")))
compat_menu("Import", lambda: open_page(os.path.join(root_path, "import")))
ui.separator()

compat_menu("Logout", lambda: user_logout() and ui.open("/login"))
compat_menu("Logout", lambda: user_logout() and ui.navigate.to("/login"))


@contextmanager
Expand All @@ -56,7 +59,9 @@ def layout(title: str | None = None, with_menu: bool = True):
root_path = get_root_path()
title = title or get_page_title(root_path)
with ui.column().classes("max-w-sm mx-auto sm:mx-0"):
custom_header()
# Experimental PWA support
if settings.ENABLE_PWA:
custom_header()

with ui.row().classes("min-w-full"):
menu_header(title, target=root_path)
Expand Down
65 changes: 65 additions & 0 deletions beaverhabits/frontend/order_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from nicegui import ui

from beaverhabits.frontend import components
from beaverhabits.frontend.components import (
HabitAddButton,
HabitDeleteButton,
HabitOrderCard,
)
from beaverhabits.frontend.layout import layout
from beaverhabits.logging import logger
from beaverhabits.storage.storage import HabitList

grid_classes = "w-full gap-0 items-center"


async def item_drop(e, habit_list: HabitList):
# Move element
elements = ui.context.client.elements
dragged = elements[int(e.args["id"][1:])]
dragged.move(target_index=e.args["new_index"])

# Update habit order
assert dragged.parent_slot is not None
habits = [
x.habit
for x in dragged.parent_slot.children
if isinstance(x, components.HabitOrderCard) and x.habit
]
habit_list.order = [str(x.id) for x in habits]
logger.info(f"New order: {habits}")


@ui.refreshable
def add_ui(habit_list: HabitList):
for item in habit_list.habits:
with components.HabitOrderCard(item):
with ui.grid(columns=7, rows=1).classes("w-full gap-0 items-center"):
name = ui.label(item.name)
name.classes("col-span-6")

delete = HabitDeleteButton(item, habit_list, add_ui.refresh)
delete.props("flat fab-mini color=grey")
delete.classes("col-span-1")


def order_page_ui(habit_list: HabitList):
with layout():
with ui.column().classes("w-full pl-1 items-center gap-3").classes("sortable"):
add_ui(habit_list)

ui.add_body_html(
r"""
<script type="module">
import '/statics/libs/sortable.min.js';
document.addEventListener('DOMContentLoaded', () => {
Sortable.create(document.querySelector('.sortable'), {
animation: 150,
ghostClass: 'opacity-50',
onEnd: (evt) => emitEvent("item_drop", {id: evt.item.id, new_index: evt.newIndex }),
});
});
</script>
"""
)
ui.on("item_drop", lambda e: item_drop(e, habit_list))
15 changes: 15 additions & 0 deletions beaverhabits/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from beaverhabits.frontend.import_page import import_ui_page
from beaverhabits.frontend.layout import custom_header
from beaverhabits.frontend.order_page import order_page_ui

from . import const, views
from .app.auth import (
Expand Down Expand Up @@ -43,6 +44,13 @@ async def demo_add_page() -> None:
add_page_ui(habit_list)


@ui.page("/demo/order")
async def demo_order_page() -> None:
days = await dummy_days(settings.INDEX_HABIT_ITEM_COUNT)
habit_list = views.get_or_create_session_habit_list(days)
order_page_ui(habit_list)


@ui.page("/demo/habits/{habit_id}")
async def demo_habit_page(habit_id: str) -> None:
today = await get_user_today_date()
Expand Down Expand Up @@ -76,6 +84,13 @@ async def add_page(user: User = Depends(current_active_user)) -> None:
add_page_ui(habits)


@ui.page("/gui/order")
async def order_page(user: User = Depends(current_active_user)) -> None:
days = await dummy_days(settings.INDEX_HABIT_ITEM_COUNT)
habits = await views.get_or_create_user_habit_list(user, days)
order_page_ui(habits)


@ui.page("/gui/habits/{habit_id}")
async def habit_page(habit_id: str, user: User = Depends(current_active_user)) -> None:
today = await get_user_today_date()
Expand Down
27 changes: 24 additions & 3 deletions beaverhabits/storage/dict.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from dataclasses import dataclass, field
from typing import Optional
import datetime
from typing import List, Optional

from beaverhabits.storage.storage import CheckedRecord, Habit, HabitList
from beaverhabits.utils import generate_short_hash
Expand Down Expand Up @@ -103,16 +103,37 @@ def __eq__(self, other: object) -> bool:
def __hash__(self) -> int:
return hash(self.id)

def __str__(self) -> str:
return f"{self.name}<{self.id}>"

__repr__ = __str__


@dataclass
class DictHabitList(HabitList[DictHabit], DictStorage):

@property
def habits(self) -> list[DictHabit]:
habits = [DictHabit(d) for d in self.data["habits"]]
if self.order:
habits.sort(
key=lambda x: (
self.order.index(str(x.id))
if str(x.id) in self.order
else float("inf")
)
)
habits.sort(key=lambda x: x.star, reverse=True)

return habits

@property
def order(self) -> List[str]:
return self.data.get("order", [])

@order.setter
def order(self, value: List[str]) -> None:
self.data["order"] = value

async def get_habit_by(self, habit_id: str) -> Optional[DictHabit]:
for habit in self.habits:
if habit.id == habit_id:
Expand Down
6 changes: 6 additions & 0 deletions beaverhabits/storage/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class HabitList[H: Habit](Protocol):
@property
def habits(self) -> List[H]: ...

@property
def order(self) -> List[str]: ...

@order.setter
def order(self, value: List[str]) -> None: ...

async def add(self, name: str) -> None: ...

async def remove(self, item: H) -> None: ...
Expand Down
Loading

0 comments on commit 937eb26

Please sign in to comment.