QuestBoard transforms your team's "To-Do" list into a guild bounty board. Pin parchment notes to a wooden canvas, assign quests to guild members, and manage your realm's progress across TODO, BLOCKED, FUTURE, and DONE quadrants.
The app uses a Serverless-on-Sheets architecture, making it completely free to host and easy to share with a team without a complex database setup.
[ Android / iOS App ]
↕ HTTPS (JSON via POST/GET)
[ Google Apps Script Web App ]
↕ Google Sheets API
[ Google Spreadsheet (Database) ]
- Create a blank spreadsheet at sheets.new.
- Copy the Spreadsheet ID from the URL (the string between
/d/and/edit).
- Go to Extensions → Apps Script.
- Rename the project to
QuestBoard_Backend. - Paste the contents of
google_apps_script/Code.gsinto the editor. - Replace
'YOUR_SPREADSHEET_ID_HERE'with your actual ID. - Deploy: Click Deploy → New Deployment.
- Type: Web app
- Execute as: Me
- Who has access: Anyone (This is required for the API to work).
- Authorize: Grant permissions to the script to manage your Sheets.
- Copy URL: Save the generated Web App URL.
Visit your Web App URL in a browser once with ?action=initSheets appended to the end, or use the Setup Screen inside the app. This creates the required Notes, Tags, and Players sheets automatically.
Ensure your images are placed in the assets/images/ folder and registered in pubspec.yaml:
assets/images/bg.jpg(The ambient background)assets/images/wood.jpg(The board texture)
- Clone the repository:
git clone https://github.com/your-username/questboard.git cd questboard - Install dependencies:
flutter pub get
- Run the app:
flutter run
- Connect: On the first launch, paste your Web App URL into the Setup Screen.
| Feature | Tech Implementation |
|---|---|
| Infinite Canvas | Native InteractiveViewer using matrix transformations for high-performance pan/zoom. |
| Parchment Cards | Custom widgets with Crimson Text and Cinzel typography for a hand-written feel. |
| Dynamic Chalk | Native Flutter rendering (Custom Containers + Shadows) to prevent SVG-scaling crashes. |
| Sketch System | image_picker for capturing sketches, converted to Base64 strings for storage in Sheets. |
| Quest Management | Draggable notes with 1:1 coordinate mapping even while zoomed. |
| The Archive | 5-second polling interval to keep all guild members in sync automatically. |
| Safe Parsing | Custom HTTP handler to bypass Google Apps Script's 302 Moved Temporarily HTML redirects. |
lib/
├── main.dart # App routing and initialization
├── models/ # NoteModel, TagModel, and BoardState (JSON Serialization)
├── services/
│ ├── api_service.dart # Handles GAS Redirects and HTTP Logic
│ └── board_provider.dart # Centralized State Management (ChangeNotifier)
├── screens/
│ ├── board_screen.dart # The primary InteractiveViewer canvas
│ └── setup_screen.dart # Configuration for the Web App URL
├── widgets/
│ ├── note_card.dart # Individual draggable sticky notes
│ ├── scroll_modal.dart # Full-screen parchment detail view
│ └── ... # Dialogs for Tags and Notes
└── theme/
└── quest_theme.dart # Global theme, Medieval colors, and Typography
| Action | Method | Usage |
|---|---|---|
getData |
GET | Returns entire board state (Notes, Tags, Players, Lock). |
saveNote |
POST | Creates a new quest or updates an existing one by ID. |
deleteNote |
POST | Permanently removes a quest from the board. |
addTag |
POST | Adds a new category and hex color to the guild repertoire. |
setLock |
POST | Toggles the board's global "Locked" state. |
"Failed to add tag" or "FormatException"
This often happens if the app receives an HTML success page instead of JSON. Ensure you are using the latest
api_service.dartwhich handles the302 Moved Temporarilyredirect behavior.
"BoxConstraints has NaN values"
This is caused by SVG scaling bugs. This project has been updated to use native Flutter drawing for chalk lines to avoid this.
"SnackBar presented off-screen"
When giant error strings (like Google login URLs) occur, the app truncates the text to prevent the SnackBar from overflowing the render box.
Notes won't drag correctly while zoomed
Drag delta is now divided by the current
_scalefactor to ensure the parchment sticks perfectly to your finger regardless of zoom level.