Skip to main content

App for Remembering Stuff

Design decisions that must be made:

  • Should empty Groups be shown or hidden in the list for remembering?
    • Advantage: no extra UI needed for adding to hidden groups
    • Disadvantage: may be ugly for nested groups
    • Workaround? allow groups to also be "resolved"

General

Create single entries

  • Simple text, one-line, nothing fancy
  • Have a state: open ( ), unanswered (?), answered (/)

Create groupings (all work the same, just icons? Or just use one generic grouping type, like folder?):

  • People
  • Groups
  • Etc.

Groupings may be recursive (how many layers?)

Views

Overview

  • Div
    • div: (dynamic) of recently answered entries
    • div: list of groups and entries
      • By default only shows open, but may also show unanswered

Simplified idea of the main area, parantheses are buttons:

(+ Create new)

(v) Person 1       (+)
- Item 1           ( )
- Item 2           ( )
- (v) Grouping     (+)
  - Subitem 1      ( )
(>) Person 2       (+)

At some point the nesting will be too deep to display on the main window. At this point I would do something like Google? and Reddit, where you actually open a new page starting with the selected entry.

History

Shows everything, and allows filtering by date or state

Settings

  • How long to show answered entries?
  • Clear old entries (and groupings?)
  • idk.

WIP

import 'package:flutter/material.dart';

void main() => runApp(const EntriesApp());

const tempData = <LabeledElement>[
  Group(label: "Hello world"),
  Entry(label: "Testing"),
  Entry(label: "Open", state: Resolution.open),
  Entry(label: "Unresolved", state: Resolution.unresolved),
  Entry(label: "Resolved", state: Resolution.resolved),
  Group(label: "Group with resolved", entries: <LabeledElement>[
    Entry(label: "Resolved in group", state: Resolution.resolved),
  ]),
  Group(label: "And a real group!", entries: <LabeledElement>[
    Entry(label: "Child 1"),
    Group(label: "A SUB GROUP! RECURSION!", entries: [
      Entry(label: "And it needed some content..."),
    ]),
    Entry(label: "Child 2"),
  ]),
];

List<LabeledElement> subQuery({required List<LabeledElement> entries, required List<Resolution> states, bool includeEmpty = true}) {
  final newList = <LabeledElement>[];

  for (LabeledElement e in entries) {
    if (e is Entry && states.contains(e.state)) newList.add(e);
    if (e is Group) {
      final groupStuff = subQuery(entries: e.entries, states: states, includeEmpty: includeEmpty);
      if (groupStuff.isNotEmpty || includeEmpty) newList.add(Group(id: e.id, label: e.label, entries: groupStuff));
    }
  }

  return newList;
}

// todo use DB
List<LabeledElement> query({required List<Resolution> states, bool includeEmpty = true}) {
  return subQuery(entries: tempData, states: states, includeEmpty: includeEmpty);
}

class EntriesApp extends StatelessWidget {
  const EntriesApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: EntriesHome());
  }
}

class EntriesHome extends StatelessWidget {
  const EntriesHome();

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text("Demo Entries"),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ...query(states: [Resolution.resolved], includeEmpty: false).map((e) => EntryWidget(entry: e)),
          const SizedBox(height: 100),
          ...query(states: [Resolution.open, Resolution.unresolved]).map((e) => EntryWidget(entry: e)),
        ]
      ),
    );
  }
}

class EntryWidget extends StatelessWidget {
  const EntryWidget({required this.entry, this.states});
  
  final LabeledElement entry;
  final List<Resolution>? states;
  
  @override
  Widget build(BuildContext context) {
    if (entry is Group) return GroupWidget(group: entry as Group);
    return Row(
      children: [
        Text(entry.label),
        (entry as Entry).state.icon,
      ],
    );
  }
}

class GroupWidget extends StatelessWidget {
  const GroupWidget({required this.group});

  final Group group;

  @override
  Widget build(BuildContext context) {
    return ExpansionTile(
      title: Text(group.label),
      controlAffinity: ListTileControlAffinity.leading,
      children: group.entries.map((e) => EntryWidget(entry: e)).toList(),
    );
  }
}

class Group extends LabeledElement {
  const Group({super.id, required super.label, this.entries = const []});

  final List<LabeledElement> entries;
}

class Entry extends LabeledElement {
  const Entry({super.id, required super.label, this.state = Resolution.open});

  final Resolution state;
}

abstract interface class LabeledElement {
  const LabeledElement({this.id, required this.label});
  
  // Todo: use
  final String? id;
  final String label;
}

enum Resolution {
  open,
  resolved,
  unresolved,
}

extension ResolutionIcon on Resolution {
  Widget get icon {
    final IconData data;
    switch (this) {
      case Resolution.open: data = Icons.circle;
      case Resolution.resolved: data = Icons.check;
      case Resolution.unresolved: data = Icons.close;
    }
    return Icon(data);
  }
}

// Entries are just strings (but need a class for persistance)
// Groups are a string, with a list of strings