Initial commit from dev

This commit is contained in:
Johan Jongsma 2026-02-01 08:03:12 +00:00
commit dd160f8451
372 changed files with 52219 additions and 0 deletions

BIN
._.DS_Store Normal file

Binary file not shown.

BIN
._inou.db Normal file

Binary file not shown.

BIN
._start.sh Normal file

Binary file not shown.

BIN
._status.sh Normal file

Binary file not shown.

BIN
._stop.sh Normal file

Binary file not shown.

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
.env
*.log
.DS_Store
api/api

View File

@ -0,0 +1,59 @@
# Consultation Prompt
Handle user input related to medical consultations, appointments, and follow-ups.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
Category: {{CATEGORY}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
For consultations, use `input_config.fields`. If creating a record, use the `entries` array.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "snake_case_identifier",
"input_type": "form|checkbox",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [{"key": "...", "type": "...", "label": "..."}]},
"entries": [{"value": "...", "data": {...}}]
}
```
## Rules
- Create a `schedule` for future appointments mentioned with a day or date. Default time is "09:00".
- Create a follow-up `question` for the scheduled appointment.
- A simple past event with no follow-up should have `schedule: null`.
- `question` and UI labels must be in {{LANGUAGE}}.
- If the user provides data for a new event, populate the `entries` array with a single entry.
## Examples
Input: "I went to the dentist"
Language: en
Category: consultation
```json
{
"question": null,
"type": "dentist_visit",
"input_type": null,
"schedule": null,
"input_config": null,
"entries": [{"value": "Dentist visit", "data": {"provider": "dentist"}}]
}
```
Input: "I have an appointment with my PCP for Friday"
Language: en
Category: consultation
```json
{
"question": "Did you attend your PCP appointment today?",
"type": "pcp_appointment",
"input_type": "checkbox",
"schedule": [{"days": ["fri"], "time": "09:00"}],
"input_config": {"fields": [{"key": "completed", "type": "checkbox", "label": "Yes"}]},
"entries": [{"value": "PCP Appointment", "data": {"provider": "PCP", "date": "Friday"}}]
}
```

55
api/prompts/default.md Normal file
View File

@ -0,0 +1,55 @@
# Default Prompt
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
Category: {{CATEGORY}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
For complex activities, use `input_config.groups`. For simple forms, use `input_config.fields`.
If creating a record, use the `entries` array.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "snake_case_identifier",
"input_type": "form|checkbox",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {
"fields": [{"key": "...", "type": "...", "label": "..."}],
"groups": [{"title": "...", "fields": [{"key": "...", "type": "...", "label": "..."}]}]
},
"entries": [{"value": "...", "data": {...}}]
}
```
## Rules
- If the user provides data for a new event, populate the `entries` array.
- If the input contains multiple distinct items, create a separate object for each in the `entries` array.
- `schedule` should be `null` for one-time past events with no follow-up.
- `question` and all UI labels must be in {{LANGUAGE}}.
## Example (Supplement)
Input: "I take 2 capsules of Vit D and 500mg of Vit C everyday"
Language: en
Category: supplement
```json
{
"question": "Did you take your supplements today?",
"category": "supplement",
"type": "daily_supplements",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {
"fields": [
{"key": "vitamin_d", "label": "Vitamin D", "type": "checkbox"},
{"key": "vitamin_c", "label": "Vitamin C", "type": "checkbox"}
]
},
"entries": [
{"value": "2 capsules Vit D", "data": {"supplement": "vitamin d", "amount": 2, "unit": "capsules"}},
{"value": "500mg Vit C", "data": {"supplement": "vitamin c", "amount": 500, "unit": "mg"}}
]
}
```

105
api/prompts/exercise.md Normal file
View File

@ -0,0 +1,105 @@
# Exercise Prompt
Extract exercise/activity details.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
For complex activities with multiple parts, use the `input_config.groups` structure to group related fields.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "exercise_type",
"input_type": "form",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {
"groups": [
{
"title": "Group Title",
"fields": [{"key": "...", "type": "...", "label": "..."}]
}
]
},
"entries": [{"value": "...", "data": {...}}]
}
```
## Schedule format
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
- `time`: 24h format "08:00", "12:00", "18:00" etc.
- Daily = all 7 days
## Standard fields
- `duration`: number (minutes)
- `distance`: number (km, miles)
- `intensity`: select [Light, Moderate, Vigorous]
## Rules
- If the activity has multiple distinct parts (e.g. left and right leg), create a `group` in `input_config` for each part.
- `entries` should be used for recording initial data. If multiple parts, create multiple entries.
## Examples
Input: "walked 30 minutes"
```json
{
"question": "Did you walk today?",
"type": "walk",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [{"key": "duration", "type": "number", "label": "Duration", "unit": "min"}]},
"entries": [{"value": "30 min walk", "data": {"duration": 30, "unit": "min"}}]
}
```
Input: "For my daughter, we have a leg trainer exercise we do twice a day. The right leg is for 20 minutes at 40 degrees and speed 4. The left leg is for 20 minutes at 30 degrees and speed 4."
```json
{
"question": "Did you do the leg trainer exercises today?",
"category": "exercise",
"type": "leg_trainer",
"input_type": "form",
"schedule": [
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "09:00"},
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "17:00"}
],
"input_config": {
"groups": [
{
"title": "Right Leg",
"fields": [
{ "key": "right_leg_duration", "type": "number", "label": "Duration (min)" },
{ "key": "right_leg_angle", "type": "number", "label": "Angle (°)" },
{ "key": "right_leg_speed", "type": "number", "label": "Speed" }
]
},
{
"title": "Left Leg",
"fields": [
{ "key": "left_leg_duration", "type": "number", "label": "Duration (min)" },
{ "key": "left_leg_angle", "type": "number", "label": "Angle (°)" },
{ "key": "left_leg_speed", "type": "number", "label": "Speed" }
]
},
{
"title": "Notes",
"fields": [
{ "key": "notes", "type": "text", "label": "Session Notes" }
]
}
]
},
"entries": [
{
"value": "Leg trainer session",
"data": {
"right_leg": { "duration_min": 20, "degrees": 40, "speed": 4 },
"left_leg": { "duration_min": 20, "degrees": 30, "speed": 4 }
}
}
]
}
```

View File

@ -0,0 +1,111 @@
# Family History Prompt
Extract details about a relative's health condition.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "confirmation question in {{LANGUAGE}}",
"type": "relation_type (mother, father, etc)",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [...]},
"entry": {"value": "human readable", "data": {...}}
}
```
## Schedule
Always `null` for family history — these are one-time records, not recurring prompts.
## Type mapping (by relation)
| Relation | type |
|----------|------|
| mother, mom | mother |
| father, dad | father |
| grandmother, grandma | grandmother |
| grandfather, grandpa | grandfather |
| sister | sister |
| brother | brother |
| aunt | aunt |
| uncle | uncle |
| son | son |
| daughter | daughter |
| spouse, husband, wife | spouse |
## Entry data structure
```json
{
"relation": "mother",
"relation_detail": "maternal grandmother",
"condition": "breast cancer",
"age_at_diagnosis": 58,
"outcome": "survived",
"treatment": "mastectomy + chemo",
"current_status": "in remission",
"notes": "..."
}
```
Include only fields that can be inferred from input.
## Rules
- entry.data must be a JSON OBJECT, not a string
- schedule is always null (no recurring prompt)
## Examples
Input: "My mother had breast cancer at 58, survived"
```json
{
"question": "Mother's breast cancer recorded. Is she still in remission?",
"type": "mother",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [{"key": "current_status", "type": "select", "label": "Current status", "options": ["In remission", "Cancer-free", "Recurrence", "Deceased"]}]},
"entry": {"value": "Mother: breast cancer at 58, survived", "data": {"relation": "mother", "condition": "breast cancer", "age_at_diagnosis": 58, "outcome": "survived"}}
}
```
Input: "Father has type 2 diabetes"
```json
{
"question": "Father's diabetes recorded. How is it managed?",
"type": "father",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [{"key": "management", "type": "select", "label": "Management", "options": ["Diet controlled", "Oral medication", "Insulin", "Unknown"]}]},
"entry": {"value": "Father: type 2 diabetes", "data": {"relation": "father", "condition": "type 2 diabetes"}}
}
```
Input: "Maternal grandmother had Alzheimer's"
```json
{
"question": "Grandmother's Alzheimer's recorded. Age of onset?",
"type": "grandmother",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [{"key": "age_onset", "type": "number", "label": "Age of onset"}, {"key": "outcome", "type": "select", "label": "Outcome", "options": ["Living with condition", "Deceased"]}]},
"entry": {"value": "Maternal grandmother: Alzheimer's", "data": {"relation": "grandmother", "relation_detail": "maternal", "condition": "Alzheimer's"}}
}
```
Input: "Brother is healthy"
```json
{
"question": "Brother recorded as healthy.",
"type": "brother",
"input_type": "form",
"schedule": null,
"input_config": {"fields": []},
"entry": {"value": "Brother: healthy", "data": {"relation": "brother", "condition": "healthy"}}
}
```

113
api/prompts/fertility.md Normal file
View File

@ -0,0 +1,113 @@
# Fertility Prompt
Extract menstrual/fertility cycle details. Also handles new pregnancy announcements.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
Existing Types: {{EXISTING_TYPES}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "fertility_type",
"input_type": "form|checkbox",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [...]},
"entries": [{"value": "...", "data": {...}}]
}
```
## Special Handling for Pregnancy
If the user announces a pregnancy (e.g., "I'm pregnant"), do NOT create an `entries` array immediately. The goal is to ask a follow-up question to get more details.
- Set `schedule` to `null`.
- Set `question` to ask for the estimated due date.
- The `input_config` should be a form with a single `date` field.
## Handling Contradictions (e.g., Period during Pregnancy)
If the user reports a period and "pregnancy" is listed in the `Existing Types`, respond with care and offer gentle options.
- Set `question` to "I'm so sorry, I know this can be a difficult time. I've noted you're tracking a pregnancy and have logged a new period. How would you like me to update your status?".
- Set `category` to `fertility`.
- Set `type` to `pregnancy_inconsistency`.
- Set `input_type` to `form`.
- Set `input_config` to offer choices to update pregnancy status.
- Set `schedule` to `null`.
- `entries` should be null.
## Schedule format
- Period/cycle tracking is typically daily.
- A new pregnancy announcement is a one-time event (`schedule: null`).
## Fertility types
period, menstruation, ovulation, cycle, fertility, pms, cramps, pregnancy
## Standard fields
- flow: select [Spotting, Light, Medium, Heavy]
- pain: scale 0-5
- mood: select [Good, Okay, Low, Anxious, Irritable]
## Examples
Input: "My period started today"
Existing Types: (none yet)
```json
{
"question": "How is your period today?",
"type": "period",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
"input_config": {"fields": [
{"key": "flow", "type": "select", "label": "Flow", "options": ["Spotting", "Light", "Medium", "Heavy"]},
{"key": "pain", "type": "scale", "label": "Pain (0-5)"}
]},
"entries": [{"value": "Period started", "data": {"started": true}}]
}
```
Input: "I'm pregnant!"
Existing Types: (none yet)
```json
{
"question": "Congratulations! What is your estimated due date?",
"category": "fertility",
"type": "pregnancy",
"input_type": "form",
"schedule": null,
"input_config": {
"fields": [
{
"key": "estimated_due_date",
"type": "date",
"label": "Estimated Due Date"
}
]
}
}
```
Input: "I just had my period"
Existing Types: - fertility: [pregnancy]
```json
{
"question": "I'm so sorry, I know this can be a difficult time. I've noted you're tracking a pregnancy and have logged a new period. How would you like me to update your status?",
"category": "fertility",
"type": "pregnancy_inconsistency",
"input_type": "form",
"schedule": null,
"input_config": {
"fields": [
{
"key": "pregnancy_status",
"type": "select",
"label": "Update my status",
"options": [
"End pregnancy tracking",
"This was spotting or something else",
"Keep tracking pregnancy"
]
}
]
}
}
```

94
api/prompts/history.md Normal file
View File

@ -0,0 +1,94 @@
# History Prompt
Extract details from a past medical event (user's own history, not documented).
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "friendly confirmation question in {{LANGUAGE}}",
"type": "snake_case_identifier",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [...]},
"entry": {"value": "human readable", "data": {...}}
}
```
## Schedule
Always `null` for history — these are one-time records, not recurring prompts.
## Type mapping
| Input pattern | type |
|---------------|------|
| appendix, appendectomy | appendectomy |
| tonsils, tonsillectomy | tonsillectomy |
| broke, fracture, broken | fracture |
| chickenpox, measles, mumps | childhood_illness |
| surgery (general) | past_surgery |
| allergy | allergy |
| injury | past_injury |
## Entry data structure
```json
{
"condition": "appendectomy",
"age": 12,
"year": 2005,
"body_part": "left arm",
"severity": "normal",
"complications": "none",
"notes": "..."
}
```
Include only fields that can be inferred from input.
## Rules
- entry.data must be a JSON OBJECT, not a string
- schedule is always null (no recurring prompt)
## Examples
Input: "I had my appendix removed when I was 12"
```json
{
"question": "Appendectomy recorded. Any complications?",
"type": "appendectomy",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [{"key": "complications", "type": "text", "label": "Complications"}]},
"entry": {"value": "Appendectomy at age 12", "data": {"condition": "appendectomy", "age": 12}}
}
```
Input: "Broke my left arm in 2015"
```json
{
"question": "Left arm fracture recorded. How did it heal?",
"type": "fracture",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [{"key": "healing", "type": "select", "label": "Healing outcome", "options": ["Full recovery", "Some limitations", "Ongoing issues"]}]},
"entry": {"value": "Left arm fracture, 2015", "data": {"condition": "fracture", "body_part": "left arm", "year": 2015}}
}
```
Input: "had chickenpox as a child"
```json
{
"question": "Chickenpox recorded. Any complications?",
"type": "childhood_illness",
"input_type": "form",
"schedule": null,
"input_config": {"fields": [{"key": "complications", "type": "text", "label": "Complications (if any)"}]},
"entry": {"value": "Chickenpox in childhood", "data": {"condition": "chickenpox", "age_period": "childhood"}}
}
```

69
api/prompts/medication.md Normal file
View File

@ -0,0 +1,69 @@
# Medication Prompt
Extract medication tracking details.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
**If the user mentions multiple medications, create one entry for each in an `entries` array. The `input_config` for the follow-up prompt should also contain a separate checkbox field for each medication identified.**
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "medication_name_snake_case",
"input_type": "form",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [{"key": "med_name", "type": "checkbox", "label": "Med Name"}]},
"entries": [{"value": "...", "data": {...}}]
}
```
## Schedule format
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
- `time`: 24h format "08:00", "12:00", "18:00" etc.
- Daily = all 7 days
- "once" or one-time event = null (no schedule, no recurring prompt)
## Rules
- `entry.data` must be a JSON OBJECT, not a string.
- If multiple medications are mentioned, create a separate entry for each in the `entries` array.
- The `input_config.fields` should contain a checkbox for each medication found. The `key` should be the snake_case name of the medication, and the `label` should be the proper name.
- If only one medication is mentioned, use the `entries` array with a single element and a single checkbox in `input_config`.
## Examples
Input: "took ibuprofen 400mg"
```json
{
"question": "Did you take your ibuprofen?",
"type": "ibuprofen",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [
{"key": "ibuprofen", "type": "checkbox", "label": "Ibuprofen"}
]},
"entries": [{"value": "Ibuprofen 400mg", "data": {"medication": "ibuprofen", "dose": 400, "unit": "mg"}}]
}
```
Input: "I took my morning pills: 10mg of Lisinopril and a multivitamin."
```json
{
"question": "Did you take your morning pills?",
"type": "morning_pills",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {
"fields": [
{"key": "lisinopril", "type": "checkbox", "label": "Lisinopril"},
{"key": "multivitamin", "type": "checkbox", "label": "Multivitamin"}
]
},
"entries": [
{"value": "10mg Lisinopril", "data": {"medication": "lisinopril", "dose": 10, "unit": "mg"}},
{"value": "Multivitamin", "data": {"medication": "multivitamin"}}
]
}
```

65
api/prompts/nutrition.md Normal file
View File

@ -0,0 +1,65 @@
# Nutrition Prompt
Extract nutrition/dietary details.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
**If the user mentions multiple foods or meals, create one entry for each in an `entries` array.**
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "nutrition_type",
"input_type": "form|checkbox",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [...]},
"entries": [{"value": "...", "data": {...}}]
}
```
## Schedule format
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
- `time`: 24h format "08:00", "12:00", "18:00" etc.
- Daily = all 7 days
- "once" or one-time event = null (no schedule, no recurring prompt)
## Nutrition types
coffee, alcohol, water, caffeine, sugar, breakfast, lunch, dinner, snack, fasting
## Rules
- entry.data must be a JSON OBJECT, not a string
- If multiple items are mentioned, create a separate entry for each in the `entries` array.
- If only one item is mentioned, use the `entries` array with a single element.
- For abstinence tracking (e.g., "stopped coffee"), create a prompt to track if they avoided it.
## Examples
Input: "I stopped drinking coffee"
```json
{
"question": "Did you have coffee today?",
"type": "coffee_abstinence",
"input_type": "checkbox",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
"input_config": {"fields": [{"key": "had_coffee", "type": "checkbox", "label": "Yes"}]},
"entries": [{"value": "Tracking coffee abstinence", "data": {"abstinence": true}}]
}
```
Input: "Today for lunch I had a chicken salad and a glass of water, and for dinner I had a steak with a side of potatoes."
```json
{
"question": "What did you eat today?",
"type": "daily_food_log",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
"input_config": {"fields": [{"key": "food", "type": "text", "label": "Food"}]},
"entries": [
{"value": "Chicken salad and water for lunch", "data": {"meal": "lunch", "food": "chicken salad", "drink": "water"}},
{"value": "Steak and potatoes for dinner", "data": {"meal": "dinner", "food": "steak", "side": "potatoes"}}
]
}
```

98
api/prompts/supplement.md Normal file
View File

@ -0,0 +1,98 @@
# Supplement Prompt
Extract supplement tracking details.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "supplement_name_snake_case",
"input_type": "form|checkbox",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [...]},
"entry": {"value": "...", "data": {...}}
}
```
## Schedule format
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
- `time`: 24h format "08:00", "12:00", "18:00" etc.
- Daily = all 7 days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
- "with meals" = 3 entries: 08:00, 12:00, 18:00
- "twice a day" = 2 entries: 08:00, 20:00
- "once" or one-time event = null (no schedule, no recurring prompt)
## Common supplements
vitamin_d, vitamin_c, omega_3, fish_oil, magnesium, zinc, iron, probiotics, multivitamin, calcium, b12, folate, melatonin, collagen
## Standard fields
- `amount`: number with unit (mg, IU, mcg, tbsp, capsules)
## Rules
- entry.data must be a JSON OBJECT, not a string
- Only include fields that have values from user input
- If one-time (e.g. "got flu shot"), set schedule to null
## Examples
Input: "taking omega-3 fish oil"
```json
{
"question": "Did you take your Omega-3 today?",
"type": "omega_3",
"input_type": "checkbox",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [{"key": "taken", "type": "checkbox", "label": "Taken"}]}
}
```
Input: "I take 1 tablespoon of fish oil daily"
```json
{
"question": "Did you take your fish oil today?",
"type": "fish_oil",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [
{"key": "amount", "type": "number", "label": "Amount", "unit": "tbsp"}
]},
"entry": {"value": "1 tbsp fish oil", "data": {"amount": 1}}
}
```
Input: "vitamin D 2000 IU with breakfast and dinner"
```json
{
"question": "Did you take your Vitamin D?",
"type": "vitamin_d",
"input_type": "form",
"schedule": [
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"},
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "18:00"}
],
"input_config": {"fields": [
{"key": "amount", "type": "number", "label": "Amount", "unit": "IU"}
]},
"entry": {"value": "Vitamin D 2000 IU", "data": {"amount": 2000}}
}
```
Input: "got my flu shot today"
```json
{
"question": null,
"type": "flu_shot",
"input_type": null,
"schedule": null,
"input_config": null,
"entry": {"value": "Flu shot", "data": {"type": "flu_shot"}}
}
```

92
api/prompts/symptom.md Normal file
View File

@ -0,0 +1,92 @@
# Symptom Prompt
Extract symptom details for tracking.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "snake_case_symptom",
"input_type": "form",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [...]},
"entry": {"value": "...", "data": {...}}
}
```
## Schedule format
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
- `time`: 24h format "08:00", "12:00", "18:00" etc.
- Daily = all 7 days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
- Symptoms typically track daily until resolved
## Common symptom types
headache, migraine, back_pain, knee_pain, joint_pain, fatigue, nausea, dizziness, insomnia, anxiety, cough, congestion, fever, rash, swelling
## Standard fields for symptoms
- `severity`: scale 0-5
- `duration`: number with unit (hours, days)
- `location`: text or select
- `triggers`: text
- `notes`: text
## Rules
- entry.data must be a JSON OBJECT, not a string
- Only include fields that have values from user input
- IMPORTANT: For injuries (cut finger, twisted ankle), track HEALING not the injury event
- Use scale (0-5) for severity/pain, not 1-10
## Examples
Input: "headache today"
```json
{
"question": "How is your headache?",
"type": "headache",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
"input_config": {"fields": [
{"key": "severity", "type": "scale", "label": "Severity (0-5)"},
{"key": "location", "type": "select", "label": "Location", "options": ["Forehead", "Temples", "Back of head", "One side", "All over"]}
]},
"entry": {"value": "Headache reported", "data": {"symptom": "headache"}}
}
```
Input: "my knee hurts when climbing stairs"
```json
{
"question": "How is your knee pain today?",
"type": "knee_pain",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
"input_config": {"fields": [
{"key": "severity", "type": "scale", "label": "Pain level (0-5)"},
{"key": "trigger", "type": "select", "label": "Worse with", "options": ["Stairs", "Walking", "Standing", "Sitting", "All activities"]}
]},
"entry": {"value": "Knee pain, worse on stairs", "data": {"symptom": "knee_pain", "trigger": "stairs"}}
}
```
Input: "twisted my ankle yesterday"
```json
{
"question": "How is your ankle healing?",
"type": "ankle_injury",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
"input_config": {"fields": [
{"key": "pain", "type": "scale", "label": "Pain (0-5)"},
{"key": "swelling", "type": "select", "label": "Swelling", "options": ["None", "Mild", "Moderate", "Severe"]}
]},
"entry": {"value": "Twisted ankle", "data": {"injury": "ankle", "type": "twist"}}
}
```

69
api/prompts/triage.md Normal file
View File

@ -0,0 +1,69 @@
# Triage Prompt
Your most important task is to first determine if the user input is a valid health entry or an invalid query.
**DO NOT categorize requests for medical advice as 'symptom' or 'question'. They are invalid queries.**
- If the input is not health-related, respond with: `{"error": "not_health_related"}`
- If the input asks for medical advice (e.g., contains "what should I do?", "how do I treat?"), respond with: `{"error": "no_medical_advice"}`
---
User said: "{{INPUT}}"
If, and only if, the input is a valid health entry, classify it into a category and extract basic metadata.
IMPORTANT: Respond with ONLY the JSON object below. No explanations, no markdown, no text before or after.
```json
{
"category": "one of the categories below",
"language": "en|nl|de|hi|...",
"has_data": true/false,
"summary": "brief normalized summary"
}
```
## Invalid Query Examples
Input: "What is the capital of Mexico?"
```json
{"error": "not_health_related"}
```
Input: "I have a headache, what should I do?"
```json
{"error": "no_medical_advice"}
```
## Categories
| Category | Use when | Examples |
|----------|----------|----------|
| vital | Measurements: weight, blood pressure, temperature, heart rate | "I weigh 80kg", "BP 120/80" |
| exercise | Physical activity, workouts, steps | "walked 30 min", "did yoga" |
| medication | Taking/tracking medicines | "took ibuprofen", "started metformin" |
| supplement | Vitamins, supplements | "taking omega-3", "vitamin D" |
| nutrition | Food, drink, dietary habits, abstinence | "stopped coffee", "no alcohol", "ate breakfast" |
| fertility | Menstrual cycle, period, ovulation, pregnancy | "period started", "ovulating", "I'm pregnant" |
| symptom | Current symptoms, pain, discomfort | "headache today", "knee hurts" |
| note | General health notes | "feeling good", "stressed" |
| history | YOUR past medical events (no documentation) | "appendectomy at 12", "had chickenpox", "broke arm 2015" |
| family_history | Relatives' health conditions | "mother: breast cancer", "father: diabetes" |
| surgery | Documented surgical procedures | "scheduled for knee surgery" |
| hospitalization | Hospital stays | "admitted for pneumonia" |
| consultation | Doctor visits, appointments | "saw cardiologist today" |
| diagnosis | Active conditions being managed | "diagnosed with epilepsy" |
| device | Medical devices, implants | "VP shunt placed", "pacemaker settings" |
| therapy | Therapeutic treatments | "started physical therapy" |
| assessment | Evaluations, tests | "developmental assessment scheduled" |
| birth | Birth-related data | "born at 32 weeks" |
| imaging_finding | Imaging results | "MRI showed..." |
| eeg_finding | EEG results | "EEG normal" |
| provider | Doctors, hospitals, clinics | "Dr. Smith is my neurologist" |
| question | Open questions | "should I worry about..." |
## Rules
- `has_data`: true if user provided actual values/facts, false if just describing intent
- `summary`: normalize to third person, e.g. "walked 30 minutes" → "30 min walk"

105
api/prompts/vital.md Normal file
View File

@ -0,0 +1,105 @@
# Vital Prompt
Extract vital sign measurements.
User said: "{{INPUT}}"
Language: {{LANGUAGE}}
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
```json
{
"question": "tracking question in {{LANGUAGE}}",
"type": "vital_type",
"input_type": "form",
"schedule": [{"days": [...], "time": "HH:MM"}],
"input_config": {"fields": [...]},
"entry": {"value": "...", "data": {...}}
}
```
## Schedule format
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
- `time`: 24h format "08:00", "12:00", "18:00" etc.
- Daily = all 7 days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
- "once" or one-time measurement = null (no schedule, no recurring prompt)
## Vital types and units
| Type | Fields | Units |
|------|--------|-------|
| weight | weight | kg, lbs |
| blood_pressure | systolic, diastolic | mmHg |
| heart_rate | bpm | bpm |
| temperature | temp | °C, °F |
| blood_glucose | glucose | mg/dL, mmol/L |
| oxygen_saturation | spo2 | % |
| sleep | hours | hours |
| steps | steps | steps |
## Rules
- entry.data must be a JSON OBJECT, not a string
- Only include fields that have values from user input
## Examples
Input: "I weigh 80kg"
```json
{
"question": "What is your weight today?",
"type": "weight",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [{"key": "weight", "type": "number", "label": "Weight", "unit": "kg"}]},
"entry": {"value": "80 kg", "data": {"weight": 80, "unit": "kg"}}
}
```
Input: "blood pressure 120/80"
```json
{
"question": "What is your blood pressure?",
"type": "blood_pressure",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [
{"key": "systolic", "type": "number", "label": "Systolic", "unit": "mmHg"},
{"key": "diastolic", "type": "number", "label": "Diastolic", "unit": "mmHg"}
]},
"entry": {"value": "120/80 mmHg", "data": {"systolic": 120, "diastolic": 80, "unit": "mmHg"}}
}
```
Input: "slept 7 hours last night"
```json
{
"question": "How many hours did you sleep?",
"type": "sleep",
"input_type": "form",
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
"input_config": {"fields": [
{"key": "hours", "type": "number", "label": "Hours", "unit": "hrs", "step": 0.5},
{"key": "quality", "type": "scale", "label": "Quality (0-5)"}
]},
"entry": {"value": "7 hours sleep", "data": {"hours": 7}}
}
```
Input: "check BP morning and evening"
```json
{
"question": "What is your blood pressure?",
"type": "blood_pressure",
"input_type": "form",
"schedule": [
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"},
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}
],
"input_config": {"fields": [
{"key": "systolic", "type": "number", "label": "Systolic", "unit": "mmHg"},
{"key": "diastolic", "type": "number", "label": "Diastolic", "unit": "mmHg"}
]}
}
```

43
app/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
app/.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: android
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: ios
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: linux
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: macos
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: web
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: windows
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

105
app/FLUTTER_TASK.md Normal file
View File

@ -0,0 +1,105 @@
# Flutter Landing Page + i18n Task
## Goal
Match Go version landing page, add i18n (EN, NL, RU first)
## Visual Changes Needed
### Layout (match Go version)
- [ ] Wrap sections in `.landing-card` style (white bg, border, rounded, padding 48px)
- [ ] Hero: centered text, not left-aligned
- [ ] Hero text: "inou organizes and shares your health dossier with your AI — securely and privately."
- [ ] Hero tagline: "Your health, understood."
- [ ] CTA button: "Sign in" (or "Invite a friend" if logged in)
### Content Sections (3 cards in Go)
1. **Hero card** - tagline + CTA
2. **"You need AI for your health"** - warm prose style
3. **"The challenge"** - Data/Reality pairs format:
- "Your MRI has 4,000 slices." / "It was read in 10 minutes."
- "Your genome has millions of variants." / "All you learned was your eye color..."
- etc.
4. **"Why we built this"** - prose paragraphs
5. **Trust section** - 4-column grid:
- Never used for training
- Never shared
- Military-grade encryption
- Delete anytime
### CSS Values (from Go)
```
--bg: #F8F7F6
--bg-card: #FFFFFF
--border: #E5E2DE
--text: #1C1917
--text-muted: #78716C
--accent: #B45309
--accent-hover: #92400E
.landing-card {
padding: 48px;
border-radius: 8px;
border: 1px solid var(--border);
margin-bottom: 24px;
}
.trust-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
```
## i18n Setup
### Packages
```yaml
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.18.0
```
### Structure
```
lib/
├── l10n/
│ ├── app_en.arb # English
│ ├── app_nl.arb # Dutch
│ └── app_ru.arb # Russian
└── main.dart # Add localization delegates
```
### Key Strings to Port (landing only)
From `lang/en.yaml`:
- data_yours: "Your data stays yours"
- never_training: "Never used for training"
- never_training_desc: "Your images are never used to train AI models."
- never_shared: "Never shared"
- never_shared_desc: "We never share your data with anyone."
- encrypted: "Military-grade encryption"
- encrypted_desc: "At rest and in transit..."
- delete: "Delete anytime"
- delete_desc: "Your data, your control."
- get_started: "Get started"
### Language Switcher
- Add to InouHeader
- Dropdown with: English, Nederlands, Русский
- Store preference (SharedPreferences)
## Source Files
- Go templates: `~/dev/inou/templates/`
- Go CSS: `~/dev/inou/static/style.css`
- Go translations: `~/dev/inou/lang/*.yaml`
## Translation Strategy
- Port EN strings first (manual)
- Use cheap model to translate EN → NL, RU
- Human review later
## Priority
1. Match visual layout first
2. Add i18n scaffolding
3. Port EN strings
4. Generate NL, RU translations

16
app/README.md Normal file
View File

@ -0,0 +1,16 @@
# inou_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
app/analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
app/android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.inou.inou_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.inou.inou_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="inou_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.inou.inou_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
app/android/build.gradle Normal file
View File

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

BIN
app/fonts/Sora-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/fonts/Sora-Light.ttf Normal file

Binary file not shown.

BIN
app/fonts/Sora-Regular.ttf Normal file

Binary file not shown.

BIN
app/fonts/Sora-SemiBold.ttf Normal file

Binary file not shown.

BIN
app/fonts/Sora-Thin.ttf Normal file

Binary file not shown.

34
app/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
app/ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Inou App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>inou_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

4
app/l10n.yaml Normal file
View File

@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Manages app locale with persistence
class LocaleProvider extends ChangeNotifier {
static const String _localeKey = 'app_locale';
Locale _locale = const Locale('en');
Locale get locale => _locale;
/// Supported locales
static const List<Locale> supportedLocales = [
Locale('en'), // English
Locale('nl'), // Dutch
Locale('ru'), // Russian
];
/// Locale display names
static const Map<String, String> localeNames = {
'en': 'English',
'nl': 'Nederlands',
'ru': 'Русский',
};
/// Short codes for display in header
static const Map<String, String> localeCodes = {
'en': 'EN',
'nl': 'NL',
'ru': 'RU',
};
LocaleProvider() {
_loadLocale();
}
/// Load saved locale from preferences
Future<void> _loadLocale() async {
try {
final prefs = await SharedPreferences.getInstance();
final localeCode = prefs.getString(_localeKey);
if (localeCode != null && supportedLocales.any((l) => l.languageCode == localeCode)) {
_locale = Locale(localeCode);
notifyListeners();
}
} catch (e) {
// Fallback to default if loading fails
debugPrint('Failed to load locale: $e');
}
}
/// Set and persist locale
Future<void> setLocale(Locale locale) async {
if (!supportedLocales.contains(locale)) return;
_locale = locale;
notifyListeners();
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, locale.languageCode);
} catch (e) {
debugPrint('Failed to save locale: $e');
}
}
/// Get current locale name for display
String get currentLocaleName => localeNames[_locale.languageCode] ?? 'English';
/// Get current locale code for header display
String get currentLocaleCode => localeCodes[_locale.languageCode] ?? 'EN';
}

116
app/lib/core/router.dart Normal file
View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Feature screens
import 'package:inou_app/features/static/landing_page.dart';
import 'package:inou_app/features/static/security_page.dart';
import 'package:inou_app/features/static/privacy_page.dart';
import 'package:inou_app/features/static/faq_page.dart';
import 'package:inou_app/features/static/dpa_page.dart';
import 'package:inou_app/features/static/connect_page.dart';
import 'package:inou_app/features/static/invite_page.dart';
import 'package:inou_app/features/auth/login_page.dart';
import 'package:inou_app/features/auth/signup_page.dart';
import 'package:inou_app/features/dashboard/dashboard.dart';
import 'package:inou_app/design/screens/styleguide_screen.dart';
/// App route definitions
class AppRoutes {
// Static pages
static const String landing = '/';
static const String security = '/security';
static const String privacy = '/privacy';
static const String faq = '/faq';
static const String dpa = '/dpa';
static const String connect = '/connect';
static const String invite = '/invite';
// Auth pages
static const String login = '/login';
static const String signup = '/signup';
static const String forgotPassword = '/forgot-password';
// Authenticated pages (deep pages)
static const String dashboard = '/dashboard';
static const String dossier = '/dossier/:id';
static const String profile = '/profile';
// Dev
static const String styleguide = '/styleguide';
}
/// GoRouter configuration
final GoRouter appRouter = GoRouter(
initialLocation: '/',
routes: [
// Static pages
GoRoute(
path: '/',
builder: (context, state) => const LandingPage(),
),
GoRoute(
path: '/security',
builder: (context, state) => const SecurityPage(),
),
GoRoute(
path: '/privacy',
builder: (context, state) => const PrivacyPage(),
),
GoRoute(
path: '/faq',
builder: (context, state) => const FaqPage(),
),
GoRoute(
path: '/dpa',
builder: (context, state) => const DpaPage(),
),
GoRoute(
path: '/connect',
builder: (context, state) => const ConnectPage(),
),
GoRoute(
path: '/invite',
builder: (context, state) => const InvitePage(),
),
// Auth pages
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/signup',
builder: (context, state) => const SignupPage(),
),
// Authenticated pages
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardPage(),
),
GoRoute(
path: '/dossier/:id',
builder: (context, state) {
final dossierId = state.pathParameters['id']!;
return DossierPage(dossierId: dossierId);
},
),
// Dev
GoRoute(
path: '/styleguide',
builder: (context, state) => const StyleguideScreen(),
),
],
errorBuilder: (context, state) => const LandingPage(),
);
/// Legacy route generator for MaterialApp (deprecated, use GoRouter)
@Deprecated('Use appRouter with GoRouter instead')
Route<dynamic>? generateRoute(RouteSettings settings) {
// Keep for backwards compatibility if needed
return MaterialPageRoute(
builder: (_) => const LandingPage(),
settings: settings,
);
}

View File

@ -0,0 +1,496 @@
/// inou Text Styles & Typography Widgets
///
/// RULES:
/// - Pages MUST use InouText.* styles or widgets
/// - NO raw TextStyle() in page code
/// - NO fontSize: or fontWeight: in page code
///
/// Usage:
/// Text('Hello', style: InouText.pageTitle)
/// InouText.pageTitle('Hello')
/// InouText.body('Paragraph', color: InouTheme.textMuted)
import 'package:flutter/material.dart';
import 'inou_theme.dart';
/// Typography system for inou
///
/// Base: 16px (1rem), Sora font, line-height 1.5
class InouText {
InouText._();
// ===========================================
// FONT FAMILY
// ===========================================
static const String fontFamily = 'Sora';
// ===========================================
// TEXT STYLES
// ===========================================
/// Page title: 2.5rem (40px), weight 800 (ExtraBold)
/// Use for: Main page headings like "Style Guide", "Privacy Policy"
static const TextStyle pageTitle = TextStyle(
fontFamily: fontFamily,
fontSize: 40.0, // 2.5 * 16
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
height: 1.2,
color: InouTheme.text,
);
/// Hero title: 2.25rem (36px), weight 300 (Light)
/// Use for: Large hero text like "Your data. Your rules."
static const TextStyle heroTitle = TextStyle(
fontFamily: fontFamily,
fontSize: 36.0, // 2.25 * 16
fontWeight: FontWeight.w300,
height: 1.2,
letterSpacing: -1.08, // -0.03em
color: InouTheme.text,
);
/// Section title: 1.4rem (22.4px), weight 600 (SemiBold)
/// Use for: Section headings like "What we collect"
static const TextStyle sectionTitle = TextStyle(
fontFamily: fontFamily,
fontSize: 22.4, // 1.4 * 16
fontWeight: FontWeight.w600,
color: InouTheme.text,
);
/// Subsection title: 1.1rem (17.6px), weight 600 (SemiBold)
/// Use for: Subsection headings like "Account information"
static const TextStyle subsectionTitle = TextStyle(
fontFamily: fontFamily,
fontSize: 17.6, // 1.1 * 16
fontWeight: FontWeight.w600,
color: InouTheme.text,
);
/// H3: 1.125rem (18px), weight 500 (Medium)
/// Use for: Tertiary headings
static const TextStyle h3 = TextStyle(
fontFamily: fontFamily,
fontSize: 18.0, // 1.125 * 16
fontWeight: FontWeight.w500,
color: InouTheme.text,
);
/// Intro text: 1.15rem (18.4px), weight 300 (Light)
/// Use for: Introduction paragraphs, larger body text
static const TextStyle intro = TextStyle(
fontFamily: fontFamily,
fontSize: 18.4,
fontWeight: FontWeight.w300,
height: 1.8,
color: InouTheme.textMuted,
);
/// Body light: 1rem (16px), weight 300 (Light)
/// Use for: Long-form content, articles, descriptions
static const TextStyle bodyLight = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w300,
height: 1.5,
color: InouTheme.text,
);
/// Body regular: 1rem (16px), weight 400 (Regular)
/// Use for: UI labels, default text, buttons
static const TextStyle body = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
height: 1.5,
color: InouTheme.text,
);
/// Body small: 0.85rem (13.6px), weight 400 (Regular)
/// Use for: Secondary text, captions, helper text
static const TextStyle bodySmall = TextStyle(
fontFamily: fontFamily,
fontSize: 13.6, // 0.85 * 16
fontWeight: FontWeight.w400,
color: InouTheme.text,
);
/// Label: 1rem (16px), weight 500 (Medium)
/// Use for: Form labels, button text
static const TextStyle label = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: InouTheme.text,
);
/// Label caps: 0.75rem (12px), weight 600, uppercase
/// Use for: Category labels like "TEXT BLOCKS", "TYPOGRAPHY SCALE"
static const TextStyle labelCaps = TextStyle(
fontFamily: fontFamily,
fontSize: 12.0, // 0.75 * 16
fontWeight: FontWeight.w600,
letterSpacing: 1.2, // 0.1em
color: InouTheme.textSubtle,
);
/// Logo: 1.75rem (28px), weight 700 (Bold)
/// Use for: "inou" in header
static const TextStyle logo = TextStyle(
fontFamily: fontFamily,
fontSize: 28.0, // 1.75 * 16
fontWeight: FontWeight.w700,
letterSpacing: -0.56, // -0.02em
);
/// Logo light: 1.75rem (28px), weight 300 (Light)
/// Use for: "health" in header
static const TextStyle logoLight = TextStyle(
fontFamily: fontFamily,
fontSize: 28.0, // 1.75 * 16
fontWeight: FontWeight.w300,
letterSpacing: -0.56, // -0.02em
);
/// Logo tagline: 0.95rem (15.2px), weight 300 (Light)
/// Use for: "ai answers for you" tagline
static const TextStyle logoTagline = TextStyle(
fontFamily: fontFamily,
fontSize: 15.2, // 0.95 * 16
fontWeight: FontWeight.w300,
letterSpacing: 0.608, // 0.04em
color: InouTheme.textMuted,
);
/// Nav item: 1rem (16px), weight 400 (Regular)
/// Use for: Navigation menu items
static const TextStyle nav = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
color: InouTheme.text,
);
/// Nav item active: 1rem (16px), weight 600 (SemiBold)
/// Use for: Active navigation menu items
static const TextStyle navActive = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: InouTheme.accent,
);
/// Mono: SF Mono, 0.85rem (13.6px)
/// Use for: Code, technical data, IDs
static const TextStyle mono = TextStyle(
fontFamily: 'SF Mono',
fontFamilyFallback: ['Monaco', 'Consolas', 'monospace'],
fontSize: 13.6, // 0.85 * 16
fontWeight: FontWeight.w400,
color: InouTheme.text,
);
/// Profile name: 1.25rem (20px), weight 600 (SemiBold)
/// Use for: User names in profile cards
static const TextStyle profileName = TextStyle(
fontFamily: fontFamily,
fontSize: 20.0, // 1.25 * 16
fontWeight: FontWeight.w600,
color: InouTheme.text,
);
/// Badge: 1rem (16px), weight 500 (Medium)
/// Use for: Badge/pill text
static const TextStyle badge = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w500,
);
/// Button: 1rem (16px), weight 500 (Medium)
/// Use for: Button text
static const TextStyle button = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w500,
);
/// Input: 1rem (16px), weight 400 (Regular)
/// Use for: Text input fields
static const TextStyle input = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
color: InouTheme.text,
);
/// Input placeholder: 1rem (16px), weight 400 (Regular)
/// Use for: Placeholder text in inputs
static const TextStyle inputPlaceholder = TextStyle(
fontFamily: fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
color: InouTheme.textSubtle,
);
/// Error text: 0.85rem (13.6px), weight 400 (Regular)
/// Use for: Form validation errors
static const TextStyle error = TextStyle(
fontFamily: fontFamily,
fontSize: 13.6,
fontWeight: FontWeight.w400,
color: InouTheme.danger,
);
/// Link: inherits size, weight 400, accent color
/// Use for: Inline links
static TextStyle link({double? fontSize}) => TextStyle(
fontFamily: fontFamily,
fontSize: fontSize ?? 16.0,
fontWeight: FontWeight.w400,
color: InouTheme.accent,
decoration: TextDecoration.underline,
decorationColor: InouTheme.accent,
);
// ===========================================
// CONVENIENCE WIDGETS
// ===========================================
/// Page title widget
static Widget pageTitleText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: pageTitle.copyWith(color: color),
textAlign: textAlign,
);
}
/// Hero title widget
static Widget heroTitleText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: heroTitle.copyWith(color: color),
textAlign: textAlign,
);
}
/// Section title widget
static Widget sectionTitleText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: sectionTitle.copyWith(color: color),
textAlign: textAlign,
);
}
/// Subsection title widget
static Widget subsectionTitleText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: subsectionTitle.copyWith(color: color),
textAlign: textAlign,
);
}
/// Body text widget
static Widget bodyText(
String text, {
Color? color,
TextAlign? textAlign,
int? maxLines,
TextOverflow? overflow,
}) {
return Text(
text,
style: body.copyWith(color: color),
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
/// Body light text widget (for long-form)
static Widget bodyLightText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: bodyLight.copyWith(color: color),
textAlign: textAlign,
);
}
/// Intro text widget
static Widget introText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: intro.copyWith(color: color),
textAlign: textAlign,
);
}
/// Label caps widget (auto-uppercases)
static Widget labelCapsText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text.toUpperCase(),
style: labelCaps.copyWith(color: color),
textAlign: textAlign,
);
}
/// Mono text widget
static Widget monoText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: mono.copyWith(color: color),
textAlign: textAlign,
);
}
/// Small text widget
static Widget smallText(
String text, {
Color? color,
TextAlign? textAlign,
}) {
return Text(
text,
style: bodySmall.copyWith(color: color),
textAlign: textAlign,
);
}
/// Error text widget
static Widget errorText(
String text, {
TextAlign? textAlign,
}) {
return Text(
text,
style: error,
textAlign: textAlign,
);
}
// ===========================================
// RICH TEXT HELPERS
// ===========================================
/// Build rich text with multiple styled spans
static Widget rich(
List<InlineSpan> children, {
TextAlign? textAlign,
}) {
return Text.rich(
TextSpan(children: children),
textAlign: textAlign,
);
}
}
/// Styled text spans for use with InouText.rich()
class InouSpan {
InouSpan._();
/// Accent colored text
static TextSpan accent(String text, {TextStyle? baseStyle}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.accent),
);
}
/// Muted colored text
static TextSpan muted(String text, {TextStyle? baseStyle}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.textMuted),
);
}
/// Subtle colored text
static TextSpan subtle(String text, {TextStyle? baseStyle}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.textSubtle),
);
}
/// Bold text
static TextSpan bold(String text, {TextStyle? baseStyle}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w700),
);
}
/// SemiBold text
static TextSpan semiBold(String text, {TextStyle? baseStyle}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w600),
);
}
/// Light text
static TextSpan light(String text, {TextStyle? baseStyle}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w300),
);
}
/// Plain text (default style)
static TextSpan plain(String text, {TextStyle? style}) {
return TextSpan(text: text, style: style);
}
/// Link text
static TextSpan link(
String text, {
VoidCallback? onTap,
TextStyle? baseStyle,
}) {
return TextSpan(
text: text,
style: (baseStyle ?? InouText.body).copyWith(
color: InouTheme.accent,
decoration: TextDecoration.underline,
decorationColor: InouTheme.accent,
),
// Note: For tap handling, wrap in GestureDetector or use url_launcher
);
}
}

View File

@ -0,0 +1,317 @@
/// inou Design System
/// Source: https://inou.com/static/style.css
/// Base: 16px, Sora font, line-height 1.5
///
/// Using LOCAL Sora font (fonts/Sora-*.ttf), not google_fonts package.
import 'package:flutter/material.dart';
class InouTheme {
InouTheme._();
// Font family - local asset
static const String _fontFamily = 'Sora';
// ===========================================
// COLORS (from :root CSS variables)
// ===========================================
static const Color bg = Color(0xFFF8F7F6); // --bg
static const Color bgCard = Color(0xFFFFFFFF); // --bg-card
static const Color border = Color(0xFFE5E2DE); // --border
static const Color borderHover = Color(0xFFC4BFB8); // --border-hover
static const Color text = Color(0xFF1C1917); // --text
static const Color textMuted = Color(0xFF78716C); // --text-muted
static const Color textSubtle = Color(0xFFA8A29E); // --text-subtle
static const Color accent = Color(0xFFB45309); // --accent
static const Color accentHover = Color(0xFF92400E); // --accent-hover
static const Color accentLight = Color(0xFFFEF3C7); // --accent-light
static const Color danger = Color(0xFFDC2626); // --danger
static const Color dangerLight = Color(0xFFFEF2F2); // --danger-light
static const Color success = Color(0xFF059669); // --success
static const Color successLight = Color(0xFFECFDF5); // --success-light
// Indicator colors (data sections)
static const Color indicatorImaging = Color(0xFFB45309);
static const Color indicatorLabs = Color(0xFF059669);
static const Color indicatorUploads = Color(0xFF6366F1);
static const Color indicatorVitals = Color(0xFFEC4899);
static const Color indicatorMedications = Color(0xFF8B5CF6);
static const Color indicatorRecords = Color(0xFF06B6D4);
static const Color indicatorJournal = Color(0xFFF59E0B);
static const Color indicatorPrivacy = Color(0xFF64748B);
static const Color indicatorGenetics = Color(0xFF10B981);
// Message border colors
static const Color errorBorder = Color(0xFFFECACA); // #FECACA
static const Color infoBorder = Color(0xFFFDE68A); // #FDE68A
static const Color successBorder = Color(0xFFA7F3D0); // #A7F3D0
// ===========================================
// SPACING
// ===========================================
static const double spaceXs = 4.0;
static const double spaceSm = 8.0;
static const double spaceMd = 12.0;
static const double spaceLg = 16.0;
static const double spaceXl = 24.0;
static const double spaceXxl = 32.0;
static const double spaceXxxl = 48.0;
// ===========================================
// BORDER RADIUS
// ===========================================
static const double radiusSm = 4.0;
static const double radiusMd = 6.0;
static const double radiusLg = 8.0;
static const double radiusXl = 12.0;
static BorderRadius get borderRadiusSm => BorderRadius.circular(radiusSm);
static BorderRadius get borderRadiusMd => BorderRadius.circular(radiusMd);
static BorderRadius get borderRadiusLg => BorderRadius.circular(radiusLg);
// ===========================================
// LAYOUT
// ===========================================
static const double maxWidth = 1200.0;
static const double maxWidthNarrow = 800.0;
static const double maxWidthForm = 360.0;
// ===========================================
// TYPOGRAPHY
// CSS base: body { font-size: 16px; line-height: 1.5; font-weight: 400; }
// All rem values calculated as: rem * 16
// ===========================================
// h1: 2.25rem (36px), weight 300, line-height 1.2, letter-spacing -0.03em
static TextStyle get h1 => const TextStyle(
fontFamily: _fontFamily,
fontSize: 36.0, // 2.25 * 16
fontWeight: FontWeight.w300,
height: 1.2,
letterSpacing: -0.03 * 36.0, // -0.03em
color: text,
);
// Page title (styleguide): 2.5rem (40px), weight 700
static TextStyle get pageTitle => const TextStyle(
fontFamily: _fontFamily,
fontSize: 40.0, // 2.5 * 16
fontWeight: FontWeight.w800, // ExtraBold
letterSpacing: -0.5, // Tighter tracking to match CSS
height: 1.2, // Line height to match CSS defaults
color: text,
);
// h2: 1.5rem (24px), weight 300, letter-spacing -0.02em
static TextStyle get h2 => const TextStyle(
fontFamily: _fontFamily,
fontSize: 24.0, // 1.5 * 16
fontWeight: FontWeight.w300,
letterSpacing: -0.02 * 24.0, // -0.02em
color: text,
);
// Section title (styleguide): 1.4rem (22.4px), weight 600
static TextStyle get sectionTitle => const TextStyle(
fontFamily: _fontFamily,
fontSize: 22.4, // 1.4 * 16
fontWeight: FontWeight.w600,
color: text,
);
// h3: 1.125rem (18px), weight 500
static TextStyle get h3 => const TextStyle(
fontFamily: _fontFamily,
fontSize: 18.0, // 1.125 * 16
fontWeight: FontWeight.w500,
color: text,
);
// Subsection title (styleguide): 1.1rem (17.6px), weight 600
static TextStyle get subsectionTitle => const TextStyle(
fontFamily: _fontFamily,
fontSize: 17.6, // 1.1 * 16
fontWeight: FontWeight.w600,
color: text,
);
// Intro text: 1.15rem (18.4px), weight 300
static TextStyle get intro => const TextStyle(
fontFamily: _fontFamily,
fontSize: 18.4, // 1.15 * 16
fontWeight: FontWeight.w300,
height: 1.8,
color: textMuted,
);
// Body light (long-form): 1rem (16px), weight 300
static TextStyle get bodyLight => const TextStyle(
fontFamily: _fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w300,
height: 1.5,
color: text,
);
// Body regular (UI labels): 1rem (16px), weight 400
static TextStyle get body => const TextStyle(
fontFamily: _fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
height: 1.5,
color: text,
);
// Small text: 0.85rem (13.6px), weight 400
static TextStyle get bodySmall => const TextStyle(
fontFamily: _fontFamily,
fontSize: 13.6, // 0.85 * 16
fontWeight: FontWeight.w400,
color: text,
);
// Label/Category: 0.75rem (12px), weight 600, uppercase, letter-spacing 0.1em
static TextStyle get labelCaps => const TextStyle(
fontFamily: _fontFamily,
fontSize: 12.0, // 0.75 * 16
fontWeight: FontWeight.w600,
letterSpacing: 0.1 * 12.0, // 0.1em
color: textSubtle,
);
// Button/label: 1rem (16px), weight 500
static TextStyle get label => const TextStyle(
fontFamily: _fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: text,
);
static TextStyle get labelLarge => label; // alias
// Badge text: 1rem (16px), weight 500
static TextStyle get badge => const TextStyle(
fontFamily: _fontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w500,
);
static TextStyle get badgeText => badge; // alias
// Logo: 1.75rem (28px), weight 700, letter-spacing -0.02em
static TextStyle get logo => const TextStyle(
fontFamily: _fontFamily,
fontSize: 28.0, // 1.75 * 16
fontWeight: FontWeight.w700,
letterSpacing: -0.02 * 28.0, // -0.02em
);
// Logo tagline: 0.95rem (15.2px), weight 300, letter-spacing 0.04em
static TextStyle get logoTagline => const TextStyle(
fontFamily: _fontFamily,
fontSize: 15.2, // 0.95 * 16
fontWeight: FontWeight.w300,
letterSpacing: 0.04 * 15.2, // 0.04em
color: textMuted,
);
// Mono: SF Mono, 0.85rem (13.6px)
static TextStyle get mono => const TextStyle(
fontFamily: 'SF Mono',
fontFamilyFallback: ['Monaco', 'Consolas', 'monospace'],
fontSize: 13.6, // 0.85 * 16
fontWeight: FontWeight.w400,
color: text,
);
// Profile card h3: 1.25rem (20px)
static TextStyle get profileName => const TextStyle(
fontFamily: _fontFamily,
fontSize: 20.0, // 1.25 * 16
fontWeight: FontWeight.w600,
color: text,
);
// ===========================================
// THEME DATA
// ===========================================
static ThemeData get light => ThemeData(
useMaterial3: true,
brightness: Brightness.light,
fontFamily: _fontFamily,
scaffoldBackgroundColor: bg,
colorScheme: ColorScheme.light(
primary: accent,
onPrimary: Colors.white,
secondary: accentLight,
onSecondary: accent,
surface: bgCard,
onSurface: text,
error: danger,
onError: Colors.white,
outline: border,
),
textTheme: TextTheme(
displayLarge: pageTitle,
displayMedium: h1,
headlineMedium: sectionTitle,
headlineSmall: subsectionTitle,
bodyLarge: body,
bodyMedium: body,
bodySmall: bodySmall,
labelLarge: label,
labelSmall: labelCaps,
),
appBarTheme: AppBarTheme(
backgroundColor: bg,
foregroundColor: text,
elevation: 0,
centerTitle: false,
),
cardTheme: CardTheme(
color: bgCard,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: borderRadiusLg,
side: BorderSide(color: border),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: accent,
foregroundColor: Colors.white,
elevation: 0,
padding: EdgeInsets.symmetric(horizontal: spaceLg, vertical: spaceMd),
shape: RoundedRectangleBorder(borderRadius: borderRadiusMd),
textStyle: label,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: text,
side: BorderSide(color: border),
padding: EdgeInsets.symmetric(horizontal: spaceLg, vertical: spaceMd),
shape: RoundedRectangleBorder(borderRadius: borderRadiusMd),
textStyle: label,
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: bgCard,
border: OutlineInputBorder(
borderRadius: borderRadiusMd,
borderSide: BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: borderRadiusMd,
borderSide: BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: borderRadiusMd,
borderSide: BorderSide(color: accent, width: 1),
),
contentPadding: EdgeInsets.symmetric(horizontal: spaceMd, vertical: spaceMd),
),
dividerTheme: DividerThemeData(
color: border,
thickness: 1,
),
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
// AUTO-GENERATED widget matches web .badge
import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
enum BadgeVariant { normal, care, comingSoon, processing }
class InouBadge extends StatelessWidget {
final String text;
final BadgeVariant variant;
const InouBadge({
super.key,
required this.text,
this.variant = BadgeVariant.normal,
});
@override
Widget build(BuildContext context) {
final style = _getStyle();
final isUppercase = variant == BadgeVariant.comingSoon;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), // 2v, 8h per styleguide
decoration: BoxDecoration(
color: style.background,
borderRadius: BorderRadius.circular(InouTheme.radiusSm),
),
child: Text(
isUppercase ? text.toUpperCase() : text,
style: InouTheme.badgeText.copyWith(
fontSize: variant == BadgeVariant.comingSoon ? 10 : 15, // 15px (1rem) per styleguide
color: style.foreground,
letterSpacing: isUppercase ? 0.5 : 0,
),
),
);
}
_BadgeStyle _getStyle() {
switch (variant) {
case BadgeVariant.normal:
return _BadgeStyle(
background: InouTheme.accentLight,
foreground: InouTheme.accent,
);
case BadgeVariant.care:
return _BadgeStyle(
background: InouTheme.successLight,
foreground: InouTheme.success,
);
case BadgeVariant.comingSoon:
return _BadgeStyle(
background: InouTheme.bg,
foreground: InouTheme.textMuted,
);
case BadgeVariant.processing:
return _BadgeStyle(
background: InouTheme.accentLight,
foreground: InouTheme.accent,
);
}
}
}
class _BadgeStyle {
final Color background;
final Color foreground;
_BadgeStyle({required this.background, required this.foreground});
}

View File

@ -0,0 +1,129 @@
// AUTO-GENERATED widget matches web .btn
import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
enum ButtonVariant { primary, secondary, danger }
enum ButtonSize { regular, small }
class InouButton extends StatelessWidget {
final String text;
final ButtonVariant variant;
final ButtonSize size;
final bool fullWidth;
final VoidCallback? onPressed;
final Widget? icon;
const InouButton({
super.key,
required this.text,
this.variant = ButtonVariant.primary,
this.size = ButtonSize.regular,
this.fullWidth = false,
this.onPressed,
this.icon,
});
@override
Widget build(BuildContext context) {
final isSmall = size == ButtonSize.small;
final padding = isSmall
? const EdgeInsets.symmetric(horizontal: 12, vertical: 6)
: const EdgeInsets.symmetric(horizontal: 18, vertical: 10);
final style = _getStyle();
Widget button = TextButton(
onPressed: onPressed,
style: TextButton.styleFrom(
backgroundColor: style.background,
foregroundColor: style.foreground,
padding: padding,
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusMd,
side: style.border,
),
textStyle: InouText.label.copyWith(
fontSize: 15, // Always 15px (1rem) for both sizes per styleguide
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
icon!,
const SizedBox(width: 6),
],
Text(text),
],
),
);
if (fullWidth) {
button = SizedBox(width: double.infinity, child: button);
}
return button;
}
_ButtonStyle _getStyle() {
switch (variant) {
case ButtonVariant.primary:
return _ButtonStyle(
background: InouTheme.accent,
foreground: Colors.white,
border: BorderSide.none,
);
case ButtonVariant.secondary:
return _ButtonStyle(
background: InouTheme.bgCard,
foreground: InouTheme.text,
border: const BorderSide(color: InouTheme.border),
);
case ButtonVariant.danger:
return _ButtonStyle(
background: InouTheme.dangerLight,
foreground: InouTheme.danger,
border: BorderSide(color: InouTheme.danger.withOpacity(0.3)),
);
}
}
}
class _ButtonStyle {
final Color background;
final Color foreground;
final BorderSide border;
_ButtonStyle({
required this.background,
required this.foreground,
required this.border,
});
}
/// Icon button (matches .btn-icon)
class InouIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback? onPressed;
final Color? color;
const InouIconButton({
super.key,
required this.icon,
this.onPressed,
this.color,
});
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(icon),
onPressed: onPressed,
color: color ?? InouTheme.textSubtle,
iconSize: 20,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
);
}
}

View File

@ -0,0 +1,310 @@
// AUTO-GENERATED widget matches web .data-card
import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
import 'package:inou_app/design/widgets/inou_badge.dart';
import 'package:inou_app/design/widgets/inou_button.dart';
/// Data card with colored indicator bar
class InouCard extends StatelessWidget {
final String? title;
final String? subtitle;
final Color indicatorColor;
final Widget? trailing;
final Widget? child;
final VoidCallback? onTap;
const InouCard({
super.key,
this.title,
this.subtitle,
this.indicatorColor = InouTheme.accent,
this.trailing,
this.child,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: InouTheme.spaceLg),
decoration: BoxDecoration(
color: InouTheme.bgCard,
borderRadius: InouTheme.borderRadiusLg,
border: Border.all(color: InouTheme.border),
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (title != null)
_Header(
title: title!,
subtitle: subtitle,
indicatorColor: indicatorColor,
trailing: trailing,
),
if (child != null) child!,
],
),
);
}
}
class _Header extends StatelessWidget {
final String title;
final String? subtitle;
final Color indicatorColor;
final Widget? trailing;
const _Header({
required this.title,
this.subtitle,
required this.indicatorColor,
this.trailing,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(InouTheme.spaceLg),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: InouTheme.border),
),
),
child: Row(
children: [
Container(
width: 4,
height: 32,
decoration: BoxDecoration(
color: indicatorColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: InouTheme.spaceMd),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.toUpperCase(),
style: InouText.labelCaps,
),
if (subtitle != null)
Text(
subtitle!,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
),
),
],
),
),
if (trailing != null) trailing!,
],
),
);
}
}
/// Simple card without indicator
class InouSimpleCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
final VoidCallback? onTap;
const InouSimpleCard({
super.key,
required this.child,
this.padding,
this.onTap,
});
@override
Widget build(BuildContext context) {
final card = Container(
padding: padding ?? const EdgeInsets.all(InouTheme.spaceLg),
decoration: BoxDecoration(
color: InouTheme.bgCard,
borderRadius: InouTheme.borderRadiusLg,
border: Border.all(color: InouTheme.border),
),
child: child,
);
if (onTap != null) {
return InkWell(
onTap: onTap,
borderRadius: InouTheme.borderRadiusLg,
child: card,
);
}
return card;
}
}
/// Profile card for dashboard
class InouProfileCard extends StatelessWidget {
final String name;
final String? role;
final String? dob;
final String? sex;
final List<ProfileStat> stats;
final bool isCare;
final VoidCallback? onTap;
final VoidCallback? onEdit;
const InouProfileCard({
super.key,
required this.name,
this.role,
this.dob,
this.sex,
this.stats = const [],
this.isCare = false,
this.onTap,
this.onEdit,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: InouTheme.borderRadiusLg,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: InouTheme.bgCard,
borderRadius: InouTheme.borderRadiusLg,
border: Border.all(color: InouTheme.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(name, style: InouText.h3),
),
if (onEdit != null)
GestureDetector(
onTap: onEdit,
child: Text('', style: TextStyle(color: InouTheme.textMuted)),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
role ?? 'you',
style: InouText.bodySmall.copyWith(color: InouTheme.textSubtle),
),
if (isCare) ...[
const SizedBox(width: 8),
const InouBadge(text: 'care', variant: BadgeVariant.care),
],
],
),
if (dob != null) ...[
const SizedBox(height: 8),
Text(
'Born: $dob${sex != null ? ' · $sex' : ''}',
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
),
],
if (stats.isNotEmpty) ...[
const SizedBox(height: 12),
Wrap(
spacing: 16,
runSpacing: 8,
children: stats.map((s) => _StatChip(stat: s)).toList(),
),
],
const Spacer(),
InouButton(
text: 'View',
size: ButtonSize.small,
onPressed: onTap,
),
],
),
),
);
}
}
class ProfileStat {
final String emoji;
final String label;
const ProfileStat(this.emoji, this.label);
}
class _StatChip extends StatelessWidget {
final ProfileStat stat;
const _StatChip({required this.stat});
@override
Widget build(BuildContext context) {
return Text(
'${stat.emoji} ${stat.label}',
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
fontSize: 12,
),
);
}
}
/// Add card (dashed border)
class InouAddCard extends StatelessWidget {
final String label;
final VoidCallback? onTap;
const InouAddCard({
super.key,
required this.label,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: InouTheme.borderRadiusLg,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: InouTheme.borderRadiusLg,
border: Border.all(
color: InouTheme.border,
width: 2,
style: BorderStyle.solid, // Note: Flutter doesn't support dashed directly
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'+',
style: TextStyle(
fontSize: 28,
color: InouTheme.accent,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 6),
Text(
label,
style: InouText.body.copyWith(color: InouTheme.textMuted),
),
],
),
),
);
}
}

View File

@ -0,0 +1,233 @@
// AUTO-GENERATED widget matches web .data-row
import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
/// Expandable data row (for imaging, labs, etc.)
class InouDataRow extends StatefulWidget {
final String label;
final String? meta;
final String? date;
final String? value;
final bool isExpandable;
final List<Widget>? children;
final Widget? leading;
final Widget? trailing;
final VoidCallback? onTap;
final bool initiallyExpanded;
const InouDataRow({
super.key,
required this.label,
this.meta,
this.date,
this.value,
this.isExpandable = false,
this.children,
this.leading,
this.trailing,
this.onTap,
this.initiallyExpanded = false,
});
@override
State<InouDataRow> createState() => _InouDataRowState();
}
class _InouDataRowState extends State<InouDataRow> {
late bool _expanded;
@override
void initState() {
super.initState();
_expanded = widget.initiallyExpanded;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
InkWell(
onTap: widget.isExpandable
? () => setState(() => _expanded = !_expanded)
: widget.onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: InouTheme.spaceLg,
vertical: InouTheme.spaceMd,
),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: InouTheme.border,
style: BorderStyle.solid,
),
),
),
child: Row(
children: [
if (widget.isExpandable)
SizedBox(
width: 20,
child: Text(
_expanded ? '' : '+',
style: TextStyle(
color: InouTheme.textMuted,
fontSize: 14,
fontFamily: 'monospace',
),
),
)
else if (widget.leading == null)
const SizedBox(width: 32),
if (widget.leading != null) ...[
widget.leading!,
const SizedBox(width: 12),
],
Expanded(
child: Text(
widget.label,
style: InouText.body.copyWith(
fontWeight: FontWeight.w500,
),
),
),
if (widget.value != null)
Text(
widget.value!,
style: TextStyle(
fontFamily: 'SF Mono',
fontSize: 13,
color: InouTheme.text,
),
),
if (widget.meta != null) ...[
const SizedBox(width: 16),
Text(
widget.meta!,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
),
),
],
if (widget.date != null) ...[
const SizedBox(width: 16),
Text(
widget.date!,
style: TextStyle(
fontFamily: 'SF Mono',
fontSize: 12,
color: InouTheme.textMuted,
),
),
],
if (widget.trailing != null) ...[
const SizedBox(width: 8),
widget.trailing!,
],
],
),
),
),
if (_expanded && widget.children != null)
Container(
color: InouTheme.bg,
child: Column(children: widget.children!),
),
],
);
}
}
/// Child row (indented)
class InouChildRow extends StatelessWidget {
final String label;
final String? value;
final String? meta;
final Widget? trailing;
final Color? valueColor;
const InouChildRow({
super.key,
required this.label,
this.value,
this.meta,
this.trailing,
this.valueColor,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: InouTheme.spaceLg,
vertical: InouTheme.spaceMd,
),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: InouTheme.border,
style: BorderStyle.solid,
),
),
),
child: Row(
children: [
const SizedBox(width: InouTheme.spaceXxxl), // 48px indent per styleguide
Expanded(
child: Text(
label,
style: InouText.body,
),
),
if (value != null)
Text(
value!,
style: TextStyle(
fontFamily: 'SF Mono',
fontSize: 13,
color: valueColor ?? InouTheme.text,
),
),
if (meta != null) ...[
const SizedBox(width: 16),
Text(
meta!,
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
),
],
if (trailing != null) ...[
const SizedBox(width: 8),
trailing!,
],
],
),
);
}
}
/// Icon for notes/vitals
class InouNoteIcon extends StatelessWidget {
final String emoji;
final Color color;
const InouNoteIcon({
super.key,
required this.emoji,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
alignment: Alignment.center,
child: Text(emoji, style: const TextStyle(fontSize: 16)),
);
}
}

View File

@ -0,0 +1,291 @@
import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
/// Footer link group
class FooterLinkGroup {
final String title;
final List<FooterLink> links;
const FooterLinkGroup({
required this.title,
required this.links,
});
}
/// Individual footer link
class FooterLink {
final String label;
final String route;
final bool isExternal;
const FooterLink({
required this.label,
required this.route,
this.isExternal = false,
});
}
/// inou Footer - responsive, matches web design
class InouFooter extends StatelessWidget {
final List<FooterLinkGroup> linkGroups;
final Function(FooterLink)? onLinkTap;
final String? copyrightText;
const InouFooter({
super.key,
this.linkGroups = const [],
this.onLinkTap,
this.copyrightText,
});
static final defaultLinkGroups = [
const FooterLinkGroup(
title: 'Product',
links: [
FooterLink(label: 'Features', route: '/features'),
FooterLink(label: 'Security', route: '/security'),
FooterLink(label: 'FAQ', route: '/faq'),
],
),
const FooterLinkGroup(
title: 'Legal',
links: [
FooterLink(label: 'Privacy', route: '/privacy'),
FooterLink(label: 'Terms', route: '/terms'),
FooterLink(label: 'DPA', route: '/dpa'),
],
),
const FooterLinkGroup(
title: 'Connect',
links: [
FooterLink(label: 'Contact', route: '/contact'),
FooterLink(label: 'Invite a friend', route: '/invite'),
],
),
];
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isMobile = screenWidth < 768;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: InouTheme.bgCard,
border: Border(
top: BorderSide(color: InouTheme.border, width: 1),
),
),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 24,
vertical: isMobile ? 32 : 48,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
isMobile
? _buildMobileLinks(context)
: _buildDesktopLinks(context),
const SizedBox(height: 32),
_buildBottomBar(context, isMobile),
],
),
),
),
);
}
Widget _buildDesktopLinks(BuildContext context) {
final groups = linkGroups.isEmpty ? defaultLinkGroups : linkGroups;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo and tagline
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'inou',
style: InouText.h3.copyWith(
fontWeight: FontWeight.w700,
color: InouTheme.accent,
letterSpacing: -0.5,
),
),
const SizedBox(height: 12),
Text(
'Your health, understood.',
style: InouText.body.copyWith(
color: InouTheme.textMuted,
fontWeight: FontWeight.w300,
),
),
],
),
),
// Link groups
for (final group in groups) ...[
const SizedBox(width: 48),
Expanded(
child: _buildLinkGroup(context, group),
),
],
],
);
}
Widget _buildMobileLinks(BuildContext context) {
final groups = linkGroups.isEmpty ? defaultLinkGroups : linkGroups;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo
Text(
'inou',
style: InouText.h3.copyWith(
fontWeight: FontWeight.w700,
color: InouTheme.accent,
letterSpacing: -0.5,
),
),
const SizedBox(height: 8),
Text(
'Your health, understood.',
style: InouText.body.copyWith(
color: InouTheme.textMuted,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 32),
// Links in 2-column grid
Wrap(
spacing: 32,
runSpacing: 24,
children: [
for (final group in groups)
SizedBox(
width: 140,
child: _buildLinkGroup(context, group),
),
],
),
],
);
}
Widget _buildLinkGroup(BuildContext context, FooterLinkGroup group) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
group.title.toUpperCase(),
style: InouText.labelCaps.copyWith(
color: InouTheme.textMuted,
letterSpacing: 1.2,
),
),
const SizedBox(height: 12),
for (final link in group.links)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () => _handleLinkTap(context, link),
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
link.label,
style: InouText.bodySmall.copyWith(
color: InouTheme.text,
),
),
if (link.isExternal) ...[
const SizedBox(width: 4),
Icon(
Icons.open_in_new,
size: 12,
color: InouTheme.textMuted,
),
],
],
),
),
),
),
],
);
}
Widget _buildBottomBar(BuildContext context, bool isMobile) {
final year = DateTime.now().year;
final copyright = copyrightText ?? '© $year inou health. All rights reserved.';
return Container(
padding: const EdgeInsets.only(top: 24),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: InouTheme.border, width: 1),
),
),
child: isMobile
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
copyright,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
),
),
const SizedBox(height: 12),
_buildSocialLinks(),
],
)
: Row(
children: [
Text(
copyright,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
),
),
const Spacer(),
_buildSocialLinks(),
],
),
);
}
Widget _buildSocialLinks() {
// Placeholder for social links if needed
return const SizedBox.shrink();
}
void _handleLinkTap(BuildContext context, FooterLink link) {
if (onLinkTap != null) {
onLinkTap!(link);
return;
}
if (link.isExternal) {
// Handle external links (url_launcher)
return;
}
Navigator.pushNamed(context, link.route);
}
}

View File

@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
import 'package:inou_app/main.dart';
import 'package:inou_app/core/locale_provider.dart';
/// Navigation item for header
class NavItem {
final String label;
final String route;
final bool isExternal;
const NavItem({
required this.label,
required this.route,
this.isExternal = false,
});
}
/// inou Header - responsive, matches web design with language switcher
class InouHeader extends StatelessWidget {
final VoidCallback? onLogoTap;
final List<NavItem> navItems;
final String? currentRoute;
final VoidCallback? onLoginTap;
final VoidCallback? onSignupTap;
final bool isLoggedIn;
final String? userName;
final VoidCallback? onProfileTap;
final VoidCallback? onLogoutTap;
const InouHeader({
super.key,
this.onLogoTap,
this.navItems = const [],
this.currentRoute,
this.onLoginTap,
this.onSignupTap,
this.isLoggedIn = false,
this.userName,
this.onProfileTap,
this.onLogoutTap,
});
static const defaultNavItems = [
NavItem(label: 'Dossiers', route: '/dossiers'),
NavItem(label: 'Privacy', route: '/privacy'),
NavItem(label: 'Connect', route: '/connect'),
NavItem(label: 'Invite a friend', route: '/invite'),
NavItem(label: 'Demo', route: '/demo'),
];
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isMobile = screenWidth < 768;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: InouTheme.bg,
border: Border(
bottom: BorderSide(color: InouTheme.border, width: 1),
),
),
child: SafeArea(
bottom: false,
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 24,
vertical: 12,
),
child: isMobile ? _buildMobileHeader(context) : _buildDesktopHeader(context),
),
),
),
);
}
Widget _buildDesktopHeader(BuildContext context) {
return Row(
children: [
// Logo
_buildLogo(context),
const SizedBox(width: 48),
// Navigation
Expanded(
child: Row(
children: [
for (final item in navItems.isEmpty ? defaultNavItems : navItems)
_buildNavItem(context, item),
],
),
),
// Language switcher
_LanguageSwitcher(),
const SizedBox(width: 16),
// Auth buttons
_buildAuthSection(context),
],
);
}
Widget _buildMobileHeader(BuildContext context) {
return Row(
children: [
_buildLogo(context),
const Spacer(),
_LanguageSwitcher(),
const SizedBox(width: 8),
_buildMobileMenuButton(context),
],
);
}
Widget _buildLogo(BuildContext context) {
final l10n = AppLocalizations.of(context);
return GestureDetector(
onTap: onLogoTap,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'inou',
style: InouText.logo.copyWith(
color: InouTheme.accent,
),
),
Text(
'health',
style: InouText.logoLight.copyWith(
color: InouTheme.textMuted,
),
),
const SizedBox(width: 12),
Text(
l10n?.appTagline ?? 'ai answers for you',
style: InouText.logoTagline,
),
],
),
);
}
Widget _buildNavItem(BuildContext context, NavItem item) {
final isActive = currentRoute == item.route;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: InkWell(
onTap: () => _navigateTo(context, item),
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(
item.label,
style: isActive ? InouText.navActive : InouText.nav,
),
),
),
);
}
Widget _buildAuthSection(BuildContext context) {
final l10n = AppLocalizations.of(context);
if (isLoggedIn) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: onProfileTap,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
CircleAvatar(
radius: 14,
backgroundColor: InouTheme.accentLight,
child: Text(
(userName ?? 'U')[0].toUpperCase(),
style: InouText.bodySmall.copyWith(
color: InouTheme.accent,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
Text(
userName ?? 'Account',
style: InouText.body,
),
],
),
),
),
],
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: onLoginTap,
child: Text(
l10n?.signIn ?? 'Log in',
style: InouText.nav,
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: onSignupTap,
style: ElevatedButton.styleFrom(
backgroundColor: InouTheme.accent,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusMd,
),
),
child: Text(l10n?.getStarted ?? 'Get started', style: InouText.button.copyWith(color: Colors.white)),
),
],
);
}
Widget _buildMobileMenuButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.menu, color: InouTheme.text),
onPressed: () => _showMobileMenu(context),
);
}
void _showMobileMenu(BuildContext context) {
final l10n = AppLocalizations.of(context);
showModalBottomSheet(
context: context,
backgroundColor: InouTheme.bgCard,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final item in navItems.isEmpty ? defaultNavItems : navItems)
ListTile(
title: Text(item.label, style: InouText.body),
trailing: item.isExternal
? Icon(Icons.open_in_new, size: 18, color: InouTheme.textMuted)
: null,
onTap: () {
Navigator.pop(context);
_navigateTo(context, item);
},
),
const Divider(height: 32),
if (!isLoggedIn) ...[
OutlinedButton(
onPressed: () {
Navigator.pop(context);
onLoginTap?.call();
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(l10n?.signIn ?? 'Log in', style: InouText.button),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onSignupTap?.call();
},
style: ElevatedButton.styleFrom(
backgroundColor: InouTheme.accent,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(l10n?.getStarted ?? 'Get started', style: InouText.button.copyWith(color: Colors.white)),
),
] else ...[
ListTile(
leading: CircleAvatar(
backgroundColor: InouTheme.accentLight,
child: Text(
(userName ?? 'U')[0].toUpperCase(),
style: InouText.bodySmall.copyWith(color: InouTheme.accent),
),
),
title: Text(userName ?? 'Account', style: InouText.body),
onTap: () {
Navigator.pop(context);
onProfileTap?.call();
},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text('Log out', style: InouText.body),
onTap: () {
Navigator.pop(context);
onLogoutTap?.call();
},
),
],
],
),
),
),
);
}
void _navigateTo(BuildContext context, NavItem item) {
if (item.isExternal) {
// Handle external links (url_launcher would be needed)
return;
}
Navigator.pushNamed(context, item.route);
}
}
/// Language switcher dropdown matching Go version .lang-menu
class _LanguageSwitcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Locale>(
valueListenable: localeNotifier,
builder: (context, locale, _) {
final currentCode = LocaleProvider.localeCodes[locale.languageCode] ?? 'EN';
return PopupMenuButton<Locale>(
offset: const Offset(0, 40),
tooltip: 'Change language',
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusMd,
),
color: InouTheme.bgCard,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: InouTheme.border),
borderRadius: InouTheme.borderRadiusSm,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
currentCode,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 4),
Icon(
Icons.keyboard_arrow_down,
size: 16,
color: InouTheme.textMuted,
),
],
),
),
onSelected: (selectedLocale) {
InouApp.setLocale(context, selectedLocale);
},
itemBuilder: (context) => [
for (final supportedLocale in LocaleProvider.supportedLocales)
PopupMenuItem<Locale>(
value: supportedLocale,
child: Row(
children: [
Text(
LocaleProvider.localeNames[supportedLocale.languageCode] ?? '',
style: InouText.bodySmall.copyWith(
color: locale.languageCode == supportedLocale.languageCode
? InouTheme.accent
: InouTheme.text,
fontWeight: locale.languageCode == supportedLocale.languageCode
? FontWeight.w600
: FontWeight.w400,
),
),
if (locale.languageCode == supportedLocale.languageCode) ...[
const SizedBox(width: 8),
Icon(Icons.check, size: 16, color: InouTheme.accent),
],
],
),
),
],
);
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More