Merge branch 'master' into feat/operation-logs

* master:
  refactor(dialog): remove unused OnDestroy implementation from DialogAddNoteComponent
  fix(calendar): poll all calendar tasks and prevent auto-move of existing tasks
  docs: add info about how to translate stuff #5893
  refactor(calendar): replace deprecated toPromise with firstValueFrom
  build: update links to match our new organization
  add QuestArc to community plugins list
  feat(calendar): implement polling for calendar task updates and enhance data retrieval logic
  fix(heatmap): use app theme class instead of prefers-color-scheme
  fix(focus-mode): start break from banner when manual break start enabled
  feat(i18n): connect Finnish and Swedish translation files
  refactor(focus-mode): split sessionComplete$ and breakComplete$ into single-responsibility effects
  Fixing Plugin API doc on persistence

# Conflicts:
#	src/app/features/issue/store/poll-issue-updates.effects.ts
#	src/app/t.const.ts
This commit is contained in:
Johannes Millan 2026-01-05 19:12:46 +01:00
commit a42c8a4cee
68 changed files with 12901 additions and 10684 deletions

50
docs/TRANSLATING.md Normal file
View file

@ -0,0 +1,50 @@
# Translation Guide
Super Productivity uses JSON files for translations, located in `src/assets/i18n/`.
## How to Contribute
1. Find your language file in `src/assets/i18n/` (e.g., `de.json` for German)
2. Edit the JSON file directly
3. Submit a pull request
## Important Notes
### Fallback Language
**English (`en.json`) is the fallback language.** If a translation is missing or empty, the app automatically displays the English text.
### Empty Values Are Intentional
When you see empty strings (`""`), this is **intentional** - it triggers the English fallback. Do not copy the English text into empty fields unless you're providing an actual translation.
```json
{
"SOME_KEY": ""
}
```
The above will display the English text for `SOME_KEY`.
### File Format
- Nested JSON structure
- Keys use SCREAMING_SNAKE_CASE
- Keep the structure intact - only change the string values
### Example
```json
{
"G": {
"CANCEL": "Abbrechen",
"SAVE": "Speichern"
}
}
```
## Tips
- Use `en.json` as reference for context
- Keep translations concise (UI space is limited)
- Test your translations locally if possible (`ng serve`)

View file

@ -10,12 +10,11 @@ For polling GitLab Issues, you need to provide an access token.
![Personal Token](https://github.com/user-attachments/assets/76fb204e-450a-4516-9d93-897ae2a32f6d)
## Project Access Token
If you self-host GitLab or have the Premium/Ultimate license, it's possible to get a Project Access Token, which is scoped to a project.
The scope is similar to the Personal Access token, but you also set a role. To learn what each role can do, see the <a href="https://docs.gitlab.com/ee/user/permissions.html#project-planning">Documentation</a>.
If you self-host GitLab or have the Premium/Ultimate license, it's possible to get a Project Access Token, which is scoped to a project.
The scope is similar to the Personal Access token, but you also set a role. To learn what each role can do, see the <a href="https://docs.gitlab.com/ee/user/permissions.html#project-planning">Documentation</a>.
![Project Token](https://github.com/user-attachments/assets/f008f114-3d3e-450d-9301-7825222f9812)
For GitHub Personal Access Token instructions, please visit the following link:
[GitHub Access Token Instructions](https://github.com/johannesjo/super-productivity/blob/master/docs/github-access-token-instructions.md)
[GitHub Access Token Instructions](https://github.com/super-productivity/super-productivity/blob/master/docs/github-access-token-instructions.md)

View file

@ -125,7 +125,7 @@ cat /tmp/mas-profile.b64 | pbcopy
Then update the GitHub Actions secret:
1. Go to: https://github.com/johannesjo/super-productivity/settings/secrets/actions
1. Go to: https://github.com/super-productivity/super-productivity/settings/secrets/actions
2. Find `mas_provision_profile`
3. Click **Update**
4. Paste the base64-encoded content

View file

@ -274,50 +274,58 @@ Plugins that render custom UI in a sandboxed iframe.
- `updateTag(tagId, updates)` - Update tag
#### Simple Counters
Simple counters let you track lightweight metrics (e.g., daily clicks or habits) that persist and sync with your data. There are two levels: **basic** (key-value pairs for today's count) and **full model** (full CRUD on `SimpleCounter` entities with date-specific values).
##### Basic Counters
These treat counters as a simple `{ [id: string]: number }` map for today's values (auto-upserts via NgRx).
| Method | Description | Example |
|--------|-------------|---------|
| `getAllCounters()` | Get all counters as `{ [id: string]: number }` | `const counters = await PluginAPI.getAllCounters(); console.log(counters['my-key']);` |
| `getCounter(id)` | Get today's value for a counter (returns `null` if unset) | `const val = await PluginAPI.getCounter('daily-commits');` |
| `setCounter(id, value)` | Set today's value (non-negative number; validates id regex `/^[A-Za-z0-9_-]+$/`) | `await PluginAPI.setCounter('daily-commits', 5);` |
| `incrementCounter(id, incrementBy = 1)` | Increment and return new value (floors at 0) | `const newVal = await PluginAPI.incrementCounter('daily-commits', 2);` |
| `decrementCounter(id, decrementBy = 1)` | Decrement and return new value (floors at 0) | `const newVal = await PluginAPI.decrementCounter('daily-commits');` |
| `deleteCounter(id)` | Delete the counter | `await PluginAPI.deleteCounter('daily-commits');` |
| Method | Description | Example |
| --------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `getAllCounters()` | Get all counters as `{ [id: string]: number }` | `const counters = await PluginAPI.getAllCounters(); console.log(counters['my-key']);` |
| `getCounter(id)` | Get today's value for a counter (returns `null` if unset) | `const val = await PluginAPI.getCounter('daily-commits');` |
| `setCounter(id, value)` | Set today's value (non-negative number; validates id regex `/^[A-Za-z0-9_-]+$/`) | `await PluginAPI.setCounter('daily-commits', 5);` |
| `incrementCounter(id, incrementBy = 1)` | Increment and return new value (floors at 0) | `const newVal = await PluginAPI.incrementCounter('daily-commits', 2);` |
| `decrementCounter(id, decrementBy = 1)` | Decrement and return new value (floors at 0) | `const newVal = await PluginAPI.decrementCounter('daily-commits');` |
| `deleteCounter(id)` | Delete the counter | `await PluginAPI.deleteCounter('daily-commits');` |
**Example:**
```javascript
// Track daily commits
let commits = await PluginAPI.getCounter('daily-commits') ?? 0;
let commits = (await PluginAPI.getCounter('daily-commits')) ?? 0;
await PluginAPI.incrementCounter('daily-commits');
PluginAPI.showSnack({ msg: `Commits today: ${await PluginAPI.getCounter('daily-commits')}`, type: 'INFO' });
PluginAPI.showSnack({
msg: `Commits today: ${await PluginAPI.getCounter('daily-commits')}`,
type: 'INFO',
});
```
##### Full SimpleCounter Model
For advanced use: Full CRUD on counters with metadata (title, enabled state, date-specific values via `countOnDay: { [date: string]: number }`).
| Method | Description | Example |
|--------|-------------|---------|
| `getAllSimpleCounters()` | Get all as `SimpleCounter[]` | `const all = await PluginAPI.getAllSimpleCounters();` |
| `getSimpleCounter(id)` | Get one by id (returns `undefined` if not found) | `const counter = await PluginAPI.getSimpleCounter('my-id');` |
| `updateSimpleCounter(id, updates)` | Partial update (e.g., `{ title: 'New Title', countOnDay: { '2025-11-17': 10 } }`) | `await PluginAPI.updateSimpleCounter('my-id', { isEnabled: false });` |
| `toggleSimpleCounter(id)` | Toggle `isOn` state (throws if not found) | `await PluginAPI.toggleSimpleCounter('my-id');` |
| `setSimpleCounterEnabled(id, isEnabled)` | Set enabled state | `await PluginAPI.setSimpleCounterEnabled('my-id', true);` |
| `deleteSimpleCounter(id)` | Delete by id | `await PluginAPI.deleteSimpleCounter('my-id');` |
| `setSimpleCounterToday(id, value)` | Set today's value (YYYY-MM-DD) | `await PluginAPI.setSimpleCounterToday('my-id', 10);` |
| `setSimpleCounterDate(id, date, value)` | Set value for specific date (validates YYYY-MM-DD) | `await PluginAPI.setSimpleCounterDate('my-id', '2025-11-16', 5);` |
| Method | Description | Example |
| ---------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `getAllSimpleCounters()` | Get all as `SimpleCounter[]` | `const all = await PluginAPI.getAllSimpleCounters();` |
| `getSimpleCounter(id)` | Get one by id (returns `undefined` if not found) | `const counter = await PluginAPI.getSimpleCounter('my-id');` |
| `updateSimpleCounter(id, updates)` | Partial update (e.g., `{ title: 'New Title', countOnDay: { '2025-11-17': 10 } }`) | `await PluginAPI.updateSimpleCounter('my-id', { isEnabled: false });` |
| `toggleSimpleCounter(id)` | Toggle `isOn` state (throws if not found) | `await PluginAPI.toggleSimpleCounter('my-id');` |
| `setSimpleCounterEnabled(id, isEnabled)` | Set enabled state | `await PluginAPI.setSimpleCounterEnabled('my-id', true);` |
| `deleteSimpleCounter(id)` | Delete by id | `await PluginAPI.deleteSimpleCounter('my-id');` |
| `setSimpleCounterToday(id, value)` | Set today's value (YYYY-MM-DD) | `await PluginAPI.setSimpleCounterToday('my-id', 10);` |
| `setSimpleCounterDate(id, date, value)` | Set value for specific date (validates YYYY-MM-DD) | `await PluginAPI.setSimpleCounterDate('my-id', '2025-11-16', 5);` |
**Example:**
```javascript
// Create/update a habit counter
await PluginAPI.updateSimpleCounter('habit-streak', {
title: 'Daily Streak',
type: 'ClickCounter',
isEnabled: true,
countOnDay: { '2025-11-17': 1 } // Today's count
countOnDay: { '2025-11-17': 1 }, // Today's count
});
await PluginAPI.toggleSimpleCounter('habit-streak');
const counter = await PluginAPI.getSimpleCounter('habit-streak');
@ -445,11 +453,11 @@ You can persist data that will also be synced vai the `persistDataSynced` and `l
```javascript
// Save plugin data
await PluginAPI.persistDataSynced('myKey', { count: 42 });
await PluginAPI.persistDataSynced(JSON.stringify({ count: 42 }));
// Load saved data
const data = await PluginAPI.loadSyncedData('myKey');
console.log(data); // { count: 42 }
const data = await PluginAPI.loadSyncedData();
console.log(data); // '{ count: 42 }'
```
## Best Practices
@ -579,8 +587,8 @@ async function testAPI() {
- **Plugin Boilerplate**: [boilerplate-solid-js](../packages/plugin-dev/boilerplate-solid-js)
- **Example Plugins**: [plugin-dev](../packages/plugin-dev)
- **Community Plugins**:
- [counter-tester-plugin](https://github.com/Mustache-Games/counter-tester-plugin) by [Mustache Dev](https://github.com/Mustache-Games)
- [sp-reporter](https://github.com/dougcooper/sp-reporter) by [dougcooper](https://github.com/dougcooper)
- [counter-tester-plugin](https://github.com/Mustache-Games/counter-tester-plugin) by [Mustache Dev](https://github.com/Mustache-Games)
- [sp-reporter](https://github.com/dougcooper/sp-reporter) by [dougcooper](https://github.com/dougcooper)
## Contributing
@ -606,7 +614,7 @@ Happy plugin development! 🚀
```md
Can you you write me a plugin for Super Productivity that plays a beep sound every time i click on a header button (You need to add a header button via PluginAPI.registerHeaderButton).
Here are the docs: https://github.com/johannesjo/super-productivity/blob/master/docs/plugin-development.md
Here are the docs: https://github.com/super-productivity/super-productivity/blob/master/docs/plugin-development.md
Don't use any PluginAPI methods that are not listed in the guide.