feat(explore): how many learn it here, opt-in; copies published within their licence
Explore says how many people learn a published studyset on this instance (ADR 0005, rule 8): shown, never ranked, counted daily by the server from the people with an identity who chose to be counted (Settings, Privacy; settings schema 8) and studied a copy on 3 or more days of the last 30, rounded down from 5 on, so that nobody can be told apart. Only a number the instance computes itself counts; votes count for nothing. The author sees the same rounded number in the Publish sheet. A copy of a published studyset is published within the original's licence: never under "no derivatives", under a licence that keeps adaptations under it only under that one, in every studyset of the copy. Its releases say what they are based on, and Explore shows it. ADR 0005 gets an addendum with stage 3's decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5613530f82
commit
7574e42415
63 changed files with 26176 additions and 30 deletions
|
|
@ -158,7 +158,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
|||
- **The source-code link**: both licences require offering the source of what an instance runs to the people who use it. `instance.info().sourceCodeUrl` names it (the operator's `SOURCE_URL`, else this project's repository) and the app's About dialog shows it next to the licences and every package's notice. Keep it working when changing the dialog or the endpoint.
|
||||
- **Public profiles** (ADR 0005, stage 1 of the public tier): `public_profile` (`serverOnly`, never sent; the app gets a `Profile`, without the person's id) holds a handle (Mastodon's rules: 1 to 30 letters, digits or underscores, unique whatever their case through `handleKey`, a few reserved for the instance; changeable until the first publish, `firstPublishedAt`), a display name, a bio, up to 4 links and an avatar. `ProfileEndpoint` (`identified` only: a profile is for others to see) reads, saves and removes the caller's own; `ProfilesEndpoint` finds anyone's by handle. Handles are on the account domain (`ACCOUNT_DOMAIN`, else the server's public host; `instance.info().accountDomain`), which can never change once anyone follows a handle on it. An avatar is a PNG of at most 1 MB that the app draws anew from the picture chosen (`avatarPng`, so a photo's metadata never leaves the device), kept in the public storage at `avatars/<SHA-256>.png`, named by its content so its URL tells nothing about whose it is, and deleted once no profile shows it. In the app, the Profile tab holds it as a form, for people with an identity.
|
||||
- **Publishing** (ADR 0005, stage 2 of the public tier): `PublishEndpoint` (`identified` only, and a public profile: a release names its author) publishes a studyset with everything inside it as its next release. The server reads the person's synced rows (`readReleaseFiles`, `lib/src/publish/`) and writes them through the `git` command (`ReleaseRepository`, never a library linked in) as a commit on `main` and the annotated tag `v<N>` in a bare repository of the studyset's own, `<REPOSITORIES_DIR>/<public id>.git` (`published_studyset`, `serverOnly`; the public id is random, made at the first publish, never a synced row's id). A release holds a directory per studyset (the published one at the top, each inside it named by its id) with `studyset.json`, `cards/<id>.json`, `notes/<id>.json`, `paths/<id>/path.json` with `steps/<id>.json`, `pdfs/<id>.json`, and the files the content names in `files/<sha256>`, described in `files.json`; each item file holds its id, position, tag names and payload as synced, keys sorted and indented, without what is only its author's (a card's `report`, a PDF's `deadline`), and items being deleted (blank payloads) are left out. Publishing is refused (`PublishRefusedException`) without a profile, unless every studyset of it is under a licence allowing it to be passed on (`release_license.dart`; "no derivatives" allowed, and marked in the app), with a card, note or step of more than one current revision, with files not uploaded yet, when nothing changed, or for a name (`slug`: lower-case letters, digits and hyphens, one per studyset of a person, fixed from the first publish) that won't do. Versions count past both the database's highest and the tags', so a version git has and the database lost is never given again, and a release the database can't record is taken back from git. The first publish fixes the person's handle. Git runs apart from the machine's configuration (no system or user configuration, no prompts) with `gc.auto` off, so objects are never removed. Deleting a person deletes their published studysets with their repositories. In the app, Share in a studyset's action row (a lock, or a globe once published; an icon below medium windows) opens the Publish sheet (`feature/publish/`): its name, whether its licence allows publishing, what changed, its releases. Publishing syncs first, since the server builds the release from what it holds; on a refusal for revisions to settle, the app saves merged those whose changes merge (ADR 0003) and tries once more, and lists those changed differently on one side, each opening where it is settled. An author can take a studyset off Explore again (`withdraw`, `withdrawnAt`): nobody can add it, copies get no updates, its releases stay; publishing it again, changed or not, brings it back.
|
||||
- **Explore and copies** (ADR 0005, stage 3, decided 2026-10-09): `ExploreEndpoint` (`requireLogin`: anonymous people explore and learn too) lists the latest release of each published studyset not withdrawn **by fit, never by popularity** (the person's roadmap): search over titles and authors (LIKE's wildcards in what is typed are only themselves), languages (a primary one takes its regional ones), kinds of content held, the newest release first, 30 to a page; the next page only when asked for (Show more, no endless scrolling). What Explore shows of a release is kept beside its repository (`studyset_release`'s title, description, languages, credits), so listing never reads git. `release` gives out the latest release's JSON (one `git cat-file --batch` run) and `file` each file by its hash. **The app makes the copy** (`ReleaseCopier`, `feature/explore/`), as an import: through the person's own repositories, the files among their own files (asset ids in the content swapped for theirs), the tags by name among their own tags, the studysets in their tree, each list in its order; everything is read first, so a release with an item in a schema this app doesn't know is refused whole (`ReleaseUnreadable`). The server never writes into a learner's space. Every item of a copy names where it came from (`published`: publication, item, release; `core/content/published_ref.dart`; card schema 9, note 3, step 3, path 2, PDF 2, studyset 5, the top studyset also with `location`), merged per side like the rest of a card, note or step, so a later release can be taken from where the copy last took one. **A later release is offered, never applied by itself** (`CopyUpdater`, `CopyUpdateBanner` on the copy's top studyset: "v4 is out: 12 changed, 5 new, 2 withdrawn", Update): the app compares it with the release the copy last took (`release(version:)`; releases never change, so the repository caches them) item by item. Cards, notes and steps take the release's content as a revision on the one where they last took a release (`takeRelease`, as `takeDictionaryUpdate`), merging per side: what only the release changed comes in, what the learner changed stays, a side both changed is a conflict settled in the editor. Studysets, paths and PDFs, kept whole, take each field of their payload the learner left as the old release had it; one they changed stays theirs (personal fields, such as a PDF's deadline, too). New items come in where they belong, unless the learner deleted that part; tags the release added come and those it took away go, the learner's own stay; files are fetched only when the person hasn't the same file already (`AssetRepository.idOf`). What the author withdrew stays the learner's, studied on, marked (`withdrawn` in its origin; the card browser's filter Withdrawn by the author); what the learner deleted stays deleted; progress is never touched. A copy of a studyset its author withdrew is offered nothing. The server never updates synced rows itself: the sync engine (v0.0.10) stores a ByteData the server updates through it as its JSON text, not its bytes (`_updateRowWithoutRecording`), unreported upstream so far. In the app, Explore is the old app's feed: cards with title, author, the start of the description, the latest release, what it holds, the licence and Add to library (its progress, then In your library, opening the copy); a summary sheet with all of it.
|
||||
- **Explore and copies** (ADR 0005, stage 3, decided 2026-10-09): `ExploreEndpoint` (`requireLogin`: anonymous people explore and learn too) lists the latest release of each published studyset not withdrawn **by fit, never by popularity** (the person's roadmap): search over titles and authors (LIKE's wildcards in what is typed are only themselves), languages (a primary one takes its regional ones), kinds of content held, the newest release first, 30 to a page; the next page only when asked for (Show more, no endless scrolling). What Explore shows of a release is kept beside its repository (`studyset_release`'s title, description, languages, credits), so listing never reads git. `release` gives out the latest release's JSON (one `git cat-file --batch` run) and `file` each file by its hash. **The app makes the copy** (`ReleaseCopier`, `feature/explore/`), as an import: through the person's own repositories, the files among their own files (asset ids in the content swapped for theirs), the tags by name among their own tags, the studysets in their tree, each list in its order; everything is read first, so a release with an item in a schema this app doesn't know is refused whole (`ReleaseUnreadable`). The server never writes into a learner's space. Every item of a copy names where it came from (`published`: publication, item, release; `core/content/published_ref.dart`; card schema 9, note 3, step 3, path 2, PDF 2, studyset 5, the top studyset also with `location`), merged per side like the rest of a card, note or step, so a later release can be taken from where the copy last took one. **A later release is offered, never applied by itself** (`CopyUpdater`, `CopyUpdateBanner` on the copy's top studyset: "v4 is out: 12 changed, 5 new, 2 withdrawn", Update): the app compares it with the release the copy last took (`release(version:)`; releases never change, so the repository caches them) item by item. Cards, notes and steps take the release's content as a revision on the one where they last took a release (`takeRelease`, as `takeDictionaryUpdate`), merging per side: what only the release changed comes in, what the learner changed stays, a side both changed is a conflict settled in the editor. Studysets, paths and PDFs, kept whole, take each field of their payload the learner left as the old release had it; one they changed stays theirs (personal fields, such as a PDF's deadline, too). New items come in where they belong, unless the learner deleted that part; tags the release added come and those it took away go, the learner's own stay; files are fetched only when the person hasn't the same file already (`AssetRepository.idOf`). What the author withdrew stays the learner's, studied on, marked (`withdrawn` in its origin; the card browser's filter Withdrawn by the author); what the learner deleted stays deleted; progress is never touched. A copy of a studyset its author withdrew is offered nothing. **"Learn this here"** (ADR 0005, rule 8): `LearnerCountFutureCall`, daily, counts per published studyset the people of this instance with the `identified` scope who chose to be counted (`countLearning` in their settings, settings schema 8, off unless switched on in Settings › Privacy) and studied a copy of it (a studyset whose payload names it) on 3 or more days of the last 30, fresh starts left out; payloads are decoded in Dart, so one that isn't JSON counts for nothing rather than failing the count. Only `learnersShown` (rounded down: 5, 10, 20, 50, …; none below 5) ever leaves the server, in Explore and the author's Publish sheet. **A copy is published within its licence** (rule 10): the server looks up the original's (`PublishRefusal.forkForbidden` under "no derivatives"; `licenseMustStay` under a licence that keeps adaptations under it, `keepsAdaptations`, in every studyset of the copy), and the release keeps what it is based on (`basedOn`, shown in Explore); a person publishing a copy of their own is its author, and the copy of another instance's studyset goes with its location alone. The server never updates synced rows itself: the sync engine (v0.0.10) stores a ByteData the server updates through it as its JSON text, not its bytes (`_updateRowWithoutRecording`), unreported upstream so far. In the app, Explore is the old app's feed: cards with title, author, the start of the description, the latest release, what it holds, the licence and Add to library (its progress, then In your library, opening the copy); a summary sheet with all of it.
|
||||
- **Deleting a person** goes through `deletePerson` (`lib/src/auth/account.dart`), in one transaction and in ADR 0001 rule 6's order: their sessions end first (a device with a valid session would re-upload everything), then their space goes with all its content through the foreign keys, then the person with their identities. `AccountEndpoint.delete` deletes the caller; the app also calls it when an anonymous account is given up (signed out of, or switched away from to GitHub), since nobody could ever sign into it again.
|
||||
- **Pruning**: `PruningFutureCall` deletes anonymous people unused for 90 days, daily, scheduled when the server starts. Scheduling a future call always adds an entry, so the server cancels the previous one by its identifier first.
|
||||
- **One person's data per device**: `PersonFolders` wipes every other person's folder when a person's data opens: their sessions ended on this device. Opening waits for the previous person's database to close.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,28 @@ requests behind a gateway. This ADR decides it, and the order it is built in.
|
|||
to fetch it, and travels over HTTPS (stage 5), never inside an activity; no core
|
||||
function depends on federation. halky.one may later index releases, never host them.
|
||||
|
||||
## Addendum: stage 3, Explore and learning from a release (2026-10-09)
|
||||
|
||||
8. **Explore finds by fit, never by popularity.** It lists what is published by search,
|
||||
languages and kinds of content, the newest release first, a page at a time, the next
|
||||
only when asked for. Votes (ActivityPub likes) cost nothing to fake, so they count for
|
||||
nothing. Learning activity is harder to fake and is **shown, never ranked**: "10+ learn
|
||||
this here", counting only people of *this* instance (a number another instance sends
|
||||
could be anything), with an identity, who chose to be counted (opt-in), and who
|
||||
studied a copy on 3 or more days of the last 30; shown rounded, from 5 on, so that
|
||||
nobody can be told apart. Ranking by it would keep the first good studyset on top for
|
||||
good.
|
||||
9. **The app makes and updates a copy; the server never writes into a learner's
|
||||
space.** A copy is written through the learner's own repositories, as an import; each
|
||||
item names its origin (publication, item, release). A later release is offered on the
|
||||
copy with what it brings and taken at a tap: per side for cards, notes and steps (as
|
||||
rule 4 says), per field for studysets, paths and PDFs; withdrawn items stay, marked and
|
||||
studied on; what the learner deleted stays deleted.
|
||||
10. **A copy may be published within its licence:** never under "no derivatives", under a
|
||||
ShareAlike licence only under that licence; it says what it is based on. An author can
|
||||
take a studyset off Explore again: nobody can add it then, and copies get no updates;
|
||||
its releases stay.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An instance's operator chooses an account domain once, and keeps WebFinger answering
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ abstract class ExploreEntry
|
|||
this.description,
|
||||
this.frontLanguage,
|
||||
this.backLanguage,
|
||||
this.basedOn,
|
||||
required this.license,
|
||||
this.licenseHolder,
|
||||
this.licenseYear,
|
||||
|
|
@ -38,6 +39,7 @@ abstract class ExploreEntry
|
|||
required this.noteCount,
|
||||
required this.pathCount,
|
||||
required this.pdfCount,
|
||||
this.learners,
|
||||
});
|
||||
|
||||
factory ExploreEntry({
|
||||
|
|
@ -54,6 +56,7 @@ abstract class ExploreEntry
|
|||
String? description,
|
||||
String? frontLanguage,
|
||||
String? backLanguage,
|
||||
String? basedOn,
|
||||
required String license,
|
||||
String? licenseHolder,
|
||||
int? licenseYear,
|
||||
|
|
@ -62,6 +65,7 @@ abstract class ExploreEntry
|
|||
required int noteCount,
|
||||
required int pathCount,
|
||||
required int pdfCount,
|
||||
int? learners,
|
||||
}) = _ExploreEntryImpl;
|
||||
|
||||
factory ExploreEntry.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -83,6 +87,7 @@ abstract class ExploreEntry
|
|||
description: jsonSerialization['description'] as String?,
|
||||
frontLanguage: jsonSerialization['frontLanguage'] as String?,
|
||||
backLanguage: jsonSerialization['backLanguage'] as String?,
|
||||
basedOn: jsonSerialization['basedOn'] as String?,
|
||||
license: jsonSerialization['license'] as String,
|
||||
licenseHolder: jsonSerialization['licenseHolder'] as String?,
|
||||
licenseYear: jsonSerialization['licenseYear'] as int?,
|
||||
|
|
@ -91,6 +96,7 @@ abstract class ExploreEntry
|
|||
noteCount: jsonSerialization['noteCount'] as int,
|
||||
pathCount: jsonSerialization['pathCount'] as int,
|
||||
pdfCount: jsonSerialization['pdfCount'] as int,
|
||||
learners: jsonSerialization['learners'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +132,9 @@ abstract class ExploreEntry
|
|||
|
||||
String? backLanguage;
|
||||
|
||||
/// Where the published studyset it is a copy of is, for a copy.
|
||||
String? basedOn;
|
||||
|
||||
/// Its licence and credits.
|
||||
String license;
|
||||
|
||||
|
|
@ -144,6 +153,10 @@ abstract class ExploreEntry
|
|||
|
||||
int pdfCount;
|
||||
|
||||
/// How many people learn it here, rounded down (5, 10, 20, 50, 100, …);
|
||||
/// null for fewer than 5, so that nobody can be told apart.
|
||||
int? learners;
|
||||
|
||||
/// Returns a shallow copy of this [ExploreEntry]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
|
|
@ -161,6 +174,7 @@ abstract class ExploreEntry
|
|||
String? description,
|
||||
String? frontLanguage,
|
||||
String? backLanguage,
|
||||
String? basedOn,
|
||||
String? license,
|
||||
String? licenseHolder,
|
||||
int? licenseYear,
|
||||
|
|
@ -169,6 +183,7 @@ abstract class ExploreEntry
|
|||
int? noteCount,
|
||||
int? pathCount,
|
||||
int? pdfCount,
|
||||
int? learners,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -187,6 +202,7 @@ abstract class ExploreEntry
|
|||
if (description != null) 'description': description,
|
||||
if (frontLanguage != null) 'frontLanguage': frontLanguage,
|
||||
if (backLanguage != null) 'backLanguage': backLanguage,
|
||||
if (basedOn != null) 'basedOn': basedOn,
|
||||
'license': license,
|
||||
if (licenseHolder != null) 'licenseHolder': licenseHolder,
|
||||
if (licenseYear != null) 'licenseYear': licenseYear,
|
||||
|
|
@ -195,6 +211,7 @@ abstract class ExploreEntry
|
|||
'noteCount': noteCount,
|
||||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
if (learners != null) 'learners': learners,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +232,7 @@ abstract class ExploreEntry
|
|||
if (description != null) 'description': description,
|
||||
if (frontLanguage != null) 'frontLanguage': frontLanguage,
|
||||
if (backLanguage != null) 'backLanguage': backLanguage,
|
||||
if (basedOn != null) 'basedOn': basedOn,
|
||||
'license': license,
|
||||
if (licenseHolder != null) 'licenseHolder': licenseHolder,
|
||||
if (licenseYear != null) 'licenseYear': licenseYear,
|
||||
|
|
@ -223,6 +241,7 @@ abstract class ExploreEntry
|
|||
'noteCount': noteCount,
|
||||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
if (learners != null) 'learners': learners,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +268,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
String? description,
|
||||
String? frontLanguage,
|
||||
String? backLanguage,
|
||||
String? basedOn,
|
||||
required String license,
|
||||
String? licenseHolder,
|
||||
int? licenseYear,
|
||||
|
|
@ -257,6 +277,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
required int noteCount,
|
||||
required int pathCount,
|
||||
required int pdfCount,
|
||||
int? learners,
|
||||
}) : super._(
|
||||
publicId: publicId,
|
||||
location: location,
|
||||
|
|
@ -271,6 +292,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
description: description,
|
||||
frontLanguage: frontLanguage,
|
||||
backLanguage: backLanguage,
|
||||
basedOn: basedOn,
|
||||
license: license,
|
||||
licenseHolder: licenseHolder,
|
||||
licenseYear: licenseYear,
|
||||
|
|
@ -279,6 +301,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
noteCount: noteCount,
|
||||
pathCount: pathCount,
|
||||
pdfCount: pdfCount,
|
||||
learners: learners,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [ExploreEntry]
|
||||
|
|
@ -299,6 +322,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
Object? description = _Undefined,
|
||||
Object? frontLanguage = _Undefined,
|
||||
Object? backLanguage = _Undefined,
|
||||
Object? basedOn = _Undefined,
|
||||
String? license,
|
||||
Object? licenseHolder = _Undefined,
|
||||
Object? licenseYear = _Undefined,
|
||||
|
|
@ -307,6 +331,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
int? noteCount,
|
||||
int? pathCount,
|
||||
int? pdfCount,
|
||||
Object? learners = _Undefined,
|
||||
}) {
|
||||
return ExploreEntry(
|
||||
publicId: publicId ?? this.publicId,
|
||||
|
|
@ -326,6 +351,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
? frontLanguage
|
||||
: this.frontLanguage,
|
||||
backLanguage: backLanguage is String? ? backLanguage : this.backLanguage,
|
||||
basedOn: basedOn is String? ? basedOn : this.basedOn,
|
||||
license: license ?? this.license,
|
||||
licenseHolder: licenseHolder is String?
|
||||
? licenseHolder
|
||||
|
|
@ -336,6 +362,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
noteCount: noteCount ?? this.noteCount,
|
||||
pathCount: pathCount ?? this.pathCount,
|
||||
pdfCount: pdfCount ?? this.pdfCount,
|
||||
learners: learners is int? ? learners : this.learners,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ abstract class Publication
|
|||
required this.location,
|
||||
required this.releases,
|
||||
required this.withdrawn,
|
||||
this.learners,
|
||||
});
|
||||
|
||||
factory Publication({
|
||||
|
|
@ -34,6 +35,7 @@ abstract class Publication
|
|||
required String location,
|
||||
required List<_ia42v78b.ReleaseInfo> releases,
|
||||
required bool withdrawn,
|
||||
int? learners,
|
||||
}) = _PublicationImpl;
|
||||
|
||||
factory Publication.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -52,6 +54,7 @@ abstract class Publication
|
|||
withdrawn: _isc.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['withdrawn'],
|
||||
),
|
||||
learners: jsonSerialization['learners'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +75,10 @@ abstract class Publication
|
|||
/// Whether its author took it off Explore.
|
||||
bool withdrawn;
|
||||
|
||||
/// How many people learn it here, rounded as Explore shows it; null for
|
||||
/// fewer than 5.
|
||||
int? learners;
|
||||
|
||||
/// Returns a shallow copy of this [Publication]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
|
|
@ -82,6 +89,7 @@ abstract class Publication
|
|||
String? location,
|
||||
List<_ia42v78b.ReleaseInfo>? releases,
|
||||
bool? withdrawn,
|
||||
int? learners,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -93,6 +101,7 @@ abstract class Publication
|
|||
'location': location,
|
||||
'releases': releases.toJson(valueToJson: (v) => v.toJson()),
|
||||
'withdrawn': withdrawn,
|
||||
if (learners != null) 'learners': learners,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +115,7 @@ abstract class Publication
|
|||
'location': location,
|
||||
'releases': releases.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
'withdrawn': withdrawn,
|
||||
if (learners != null) 'learners': learners,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +125,8 @@ abstract class Publication
|
|||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PublicationImpl extends Publication {
|
||||
_PublicationImpl({
|
||||
required _isc.UuidValue publicId,
|
||||
|
|
@ -123,6 +135,7 @@ class _PublicationImpl extends Publication {
|
|||
required String location,
|
||||
required List<_ia42v78b.ReleaseInfo> releases,
|
||||
required bool withdrawn,
|
||||
int? learners,
|
||||
}) : super._(
|
||||
publicId: publicId,
|
||||
studysetId: studysetId,
|
||||
|
|
@ -130,6 +143,7 @@ class _PublicationImpl extends Publication {
|
|||
location: location,
|
||||
releases: releases,
|
||||
withdrawn: withdrawn,
|
||||
learners: learners,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [Publication]
|
||||
|
|
@ -143,6 +157,7 @@ class _PublicationImpl extends Publication {
|
|||
String? location,
|
||||
List<_ia42v78b.ReleaseInfo>? releases,
|
||||
bool? withdrawn,
|
||||
Object? learners = _Undefined,
|
||||
}) {
|
||||
return Publication(
|
||||
publicId: publicId ?? this.publicId,
|
||||
|
|
@ -151,6 +166,7 @@ class _PublicationImpl extends Publication {
|
|||
location: location ?? this.location,
|
||||
releases: releases ?? this.releases.map((e0) => e0.copyWith()).toList(),
|
||||
withdrawn: withdrawn ?? this.withdrawn,
|
||||
learners: learners is int? ? learners : this.learners,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,15 @@ enum PublishRefusal implements _isc.SerializableModel {
|
|||
slugFixed,
|
||||
|
||||
/// Publishing that studyset is already under way.
|
||||
busy;
|
||||
busy,
|
||||
|
||||
/// It is a copy of a published studyset whose licence forbids passing on
|
||||
/// adaptations ("no derivatives"): only its author publishes it.
|
||||
forkForbidden,
|
||||
|
||||
/// It is a copy of a published studyset under a licence that asks
|
||||
/// adaptations to stay under it (ShareAlike): `license` says which.
|
||||
licenseMustStay;
|
||||
|
||||
static PublishRefusal fromJson(String name) {
|
||||
switch (name) {
|
||||
|
|
@ -71,6 +79,10 @@ enum PublishRefusal implements _isc.SerializableModel {
|
|||
return PublishRefusal.slugFixed;
|
||||
case 'busy':
|
||||
return PublishRefusal.busy;
|
||||
case 'forkForbidden':
|
||||
return PublishRefusal.forkForbidden;
|
||||
case 'licenseMustStay':
|
||||
return PublishRefusal.licenseMustStay;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "PublishRefusal"',
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ abstract class PublishRefusedException
|
|||
PublishRefusedException._({
|
||||
required this.reason,
|
||||
this.items,
|
||||
this.license,
|
||||
});
|
||||
|
||||
factory PublishRefusedException({
|
||||
required _i5rbkflk.PublishRefusal reason,
|
||||
List<_isc.UuidValue>? items,
|
||||
String? license,
|
||||
}) = _PublishRefusedExceptionImpl;
|
||||
|
||||
factory PublishRefusedException.fromJson(
|
||||
|
|
@ -42,6 +44,7 @@ abstract class PublishRefusedException
|
|||
: _itx5j3f5.Protocol().deserialize<List<_isc.UuidValue>>(
|
||||
jsonSerialization['items'],
|
||||
),
|
||||
license: jsonSerialization['license'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -52,12 +55,16 @@ abstract class PublishRefusedException
|
|||
/// `licenseNotOpen`, the studysets whose licence doesn't allow it.
|
||||
List<_isc.UuidValue>? items;
|
||||
|
||||
/// With `licenseMustStay`, the licence the copy must keep.
|
||||
String? license;
|
||||
|
||||
/// Returns a shallow copy of this [PublishRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
PublishRefusedException copyWith({
|
||||
_i5rbkflk.PublishRefusal? reason,
|
||||
List<_isc.UuidValue>? items,
|
||||
String? license,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -65,6 +72,7 @@ abstract class PublishRefusedException
|
|||
'__className__': 'PublishRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
if (items != null) 'items': items?.toJson(valueToJson: (v) => v.toJson()),
|
||||
if (license != null) 'license': license,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -74,12 +82,13 @@ abstract class PublishRefusedException
|
|||
'__className__': 'PublishRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
if (items != null) 'items': items?.toJson(valueToJson: (v) => v.toJson()),
|
||||
if (license != null) 'license': license,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PublishRefusedException(reason: $reason, items: $items)';
|
||||
return 'PublishRefusedException(reason: $reason, items: $items, license: $license)';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,9 +98,11 @@ class _PublishRefusedExceptionImpl extends PublishRefusedException {
|
|||
_PublishRefusedExceptionImpl({
|
||||
required _i5rbkflk.PublishRefusal reason,
|
||||
List<_isc.UuidValue>? items,
|
||||
String? license,
|
||||
}) : super._(
|
||||
reason: reason,
|
||||
items: items,
|
||||
license: license,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PublishRefusedException]
|
||||
|
|
@ -101,12 +112,14 @@ class _PublishRefusedExceptionImpl extends PublishRefusedException {
|
|||
PublishRefusedException copyWith({
|
||||
_i5rbkflk.PublishRefusal? reason,
|
||||
Object? items = _Undefined,
|
||||
Object? license = _Undefined,
|
||||
}) {
|
||||
return PublishRefusedException(
|
||||
reason: reason ?? this.reason,
|
||||
items: items is List<_isc.UuidValue>?
|
||||
? items
|
||||
: this.items?.map((e0) => e0).toList(),
|
||||
license: license is String? ? license : this.license,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1441,5 +1441,13 @@
|
|||
"copyUpdated": "Auf v{version} aktualisiert.",
|
||||
"copyUpdatedToSettle": "{count, plural, =1{Bei einem hast du dort auch etwas geändert: öffne es, um zu wählen.} other{Bei {count} hast du dort auch etwas geändert: öffne sie, um zu wählen.}}",
|
||||
"copyUpdateFailed": "Deine Instanz war nicht erreichbar. Versuche es noch einmal.",
|
||||
"cardBrowserStatusWithdrawn": "Im Original zurückgezogen"
|
||||
"cardBrowserStatusWithdrawn": "Im Original zurückgezogen",
|
||||
"settingsPrivacy": "Privatsphäre",
|
||||
"settingsCountLearning": "Mein Lernen in Entdecken mitzählen",
|
||||
"settingsCountLearningSubtitle": "Entdecken zeigt, wie viele auf dieser Instanz ein veröffentlichtes Lernset lernen: Personen mit einer Identität, die das gewählt und es in den letzten 30 Tagen an 3 oder mehr Tagen gelernt haben, gerundet, ab 5. Nichts über dich wird gezeigt, und nichts verlässt die Instanz.",
|
||||
"exploreLearners": "{count}+ lernen das hier",
|
||||
"publishLearnersFew": "Weniger als 5 lernen es hier, von denen, die mitgezählt werden möchten.",
|
||||
"publishForkForbidden": "Sein Original verbietet, Bearbeitungen weiterzugeben: nur wer es verfasst hat, veröffentlicht es.",
|
||||
"publishLicenseMustStay": "Sein Original verlangt, dass Bearbeitungen unter {license} bleiben: wähle sie unter Bearbeiten, für jedes Lernset darin.",
|
||||
"exploreBasedOn": "nach {location}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1686,5 +1686,16 @@
|
|||
"copyUpdatedToSettle": "{count, plural, =1{One has a change of yours there too: open it to choose.} other{{count} have a change of yours there too: open them to choose.}}",
|
||||
"@copyUpdatedToSettle": {"placeholders": {"count": {"type": "int"}}},
|
||||
"copyUpdateFailed": "Couldn't reach your instance. Try again.",
|
||||
"cardBrowserStatusWithdrawn": "Withdrawn by the author"
|
||||
"cardBrowserStatusWithdrawn": "Withdrawn by the author",
|
||||
"settingsPrivacy": "Privacy",
|
||||
"settingsCountLearning": "Count my learning in Explore",
|
||||
"settingsCountLearningSubtitle": "Explore shows how many people on this instance learn a published studyset: those with an identity who chose this and studied it on 3 or more days in the last 30, rounded, from 5 on. Nothing about you is shown, and nothing leaves the instance.",
|
||||
"exploreLearners": "{count}+ learn this here",
|
||||
"@exploreLearners": {"placeholders": {"count": {"type": "int"}}},
|
||||
"publishLearnersFew": "Fewer than 5 learn it here, of those who chose to be counted.",
|
||||
"publishForkForbidden": "Its original forbids passing on adaptations: only its author publishes it.",
|
||||
"publishLicenseMustStay": "Its original asks adaptations to stay under {license}: choose it under Edit, for every studyset in it.",
|
||||
"@publishLicenseMustStay": {"placeholders": {"license": {"type": "String"}}},
|
||||
"exploreBasedOn": "based on {location}",
|
||||
"@exploreBasedOn": {"placeholders": {"location": {"type": "String"}}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8558,6 +8558,54 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Withdrawn by the author'**
|
||||
String get cardBrowserStatusWithdrawn;
|
||||
|
||||
/// No description provided for @settingsPrivacy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Privacy'**
|
||||
String get settingsPrivacy;
|
||||
|
||||
/// No description provided for @settingsCountLearning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Count my learning in Explore'**
|
||||
String get settingsCountLearning;
|
||||
|
||||
/// No description provided for @settingsCountLearningSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Explore shows how many people on this instance learn a published studyset: those with an identity who chose this and studied it on 3 or more days in the last 30, rounded, from 5 on. Nothing about you is shown, and nothing leaves the instance.'**
|
||||
String get settingsCountLearningSubtitle;
|
||||
|
||||
/// No description provided for @exploreLearners.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count}+ learn this here'**
|
||||
String exploreLearners(int count);
|
||||
|
||||
/// No description provided for @publishLearnersFew.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fewer than 5 learn it here, of those who chose to be counted.'**
|
||||
String get publishLearnersFew;
|
||||
|
||||
/// No description provided for @publishForkForbidden.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its original forbids passing on adaptations: only its author publishes it.'**
|
||||
String get publishForkForbidden;
|
||||
|
||||
/// No description provided for @publishLicenseMustStay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its original asks adaptations to stay under {license}: choose it under Edit, for every studyset in it.'**
|
||||
String publishLicenseMustStay(String license);
|
||||
|
||||
/// No description provided for @exploreBasedOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'based on {location}'**
|
||||
String exploreBasedOn(String location);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -5366,4 +5366,37 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get cardBrowserStatusWithdrawn => 'Im Original zurückgezogen';
|
||||
|
||||
@override
|
||||
String get settingsPrivacy => 'Privatsphäre';
|
||||
|
||||
@override
|
||||
String get settingsCountLearning => 'Mein Lernen in Entdecken mitzählen';
|
||||
|
||||
@override
|
||||
String get settingsCountLearningSubtitle =>
|
||||
'Entdecken zeigt, wie viele auf dieser Instanz ein veröffentlichtes Lernset lernen: Personen mit einer Identität, die das gewählt und es in den letzten 30 Tagen an 3 oder mehr Tagen gelernt haben, gerundet, ab 5. Nichts über dich wird gezeigt, und nichts verlässt die Instanz.';
|
||||
|
||||
@override
|
||||
String exploreLearners(int count) {
|
||||
return '$count+ lernen das hier';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publishLearnersFew =>
|
||||
'Weniger als 5 lernen es hier, von denen, die mitgezählt werden möchten.';
|
||||
|
||||
@override
|
||||
String get publishForkForbidden =>
|
||||
'Sein Original verbietet, Bearbeitungen weiterzugeben: nur wer es verfasst hat, veröffentlicht es.';
|
||||
|
||||
@override
|
||||
String publishLicenseMustStay(String license) {
|
||||
return 'Sein Original verlangt, dass Bearbeitungen unter $license bleiben: wähle sie unter Bearbeiten, für jedes Lernset darin.';
|
||||
}
|
||||
|
||||
@override
|
||||
String exploreBasedOn(String location) {
|
||||
return 'nach $location';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5318,6 +5318,39 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get cardBrowserStatusWithdrawn => 'Withdrawn by the author';
|
||||
|
||||
@override
|
||||
String get settingsPrivacy => 'Privacy';
|
||||
|
||||
@override
|
||||
String get settingsCountLearning => 'Count my learning in Explore';
|
||||
|
||||
@override
|
||||
String get settingsCountLearningSubtitle =>
|
||||
'Explore shows how many people on this instance learn a published studyset: those with an identity who chose this and studied it on 3 or more days in the last 30, rounded, from 5 on. Nothing about you is shown, and nothing leaves the instance.';
|
||||
|
||||
@override
|
||||
String exploreLearners(int count) {
|
||||
return '$count+ learn this here';
|
||||
}
|
||||
|
||||
@override
|
||||
String get publishLearnersFew =>
|
||||
'Fewer than 5 learn it here, of those who chose to be counted.';
|
||||
|
||||
@override
|
||||
String get publishForkForbidden =>
|
||||
'Its original forbids passing on adaptations: only its author publishes it.';
|
||||
|
||||
@override
|
||||
String publishLicenseMustStay(String license) {
|
||||
return 'Its original asks adaptations to stay under $license: choose it under Edit, for every studyset in it.';
|
||||
}
|
||||
|
||||
@override
|
||||
String exploreBasedOn(String location) {
|
||||
return 'based on $location';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class ServerpodExploreGateway(final protocol.Client _client)
|
|||
description: _description(entry.description),
|
||||
frontLanguage: entry.frontLanguage,
|
||||
backLanguage: entry.backLanguage,
|
||||
basedOn: entry.basedOn,
|
||||
license: entry.license,
|
||||
licenseHolder: entry.licenseHolder,
|
||||
licenseYear: entry.licenseYear,
|
||||
|
|
@ -91,6 +92,7 @@ class ServerpodExploreGateway(final protocol.Client _client)
|
|||
noteCount: entry.noteCount,
|
||||
paths: entry.pathCount,
|
||||
pdfs: entry.pdfCount,
|
||||
learners: entry.learners,
|
||||
);
|
||||
|
||||
/// A description as the instance keeps it, its rich text's JSON; empty
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ final class const ExploreStudyset({
|
|||
final String? frontLanguage,
|
||||
final String? backLanguage,
|
||||
|
||||
/// Where the published studyset it is a copy of is, for a copy.
|
||||
final String? basedOn,
|
||||
|
||||
/// Its licence and credits.
|
||||
required final String license,
|
||||
final String? licenseHolder,
|
||||
|
|
@ -41,6 +44,10 @@ final class const ExploreStudyset({
|
|||
final int noteCount = 0,
|
||||
final int paths = 0,
|
||||
final int pdfs = 0,
|
||||
|
||||
/// How many people learn it here, rounded (5, 10, 20, 50, …); null for
|
||||
/// fewer than 5 (ADR 0005: shown, never ranked).
|
||||
final int? learners,
|
||||
}) with Equatable {
|
||||
/// Its author as they are shown: their display name, else their handle.
|
||||
String get author => displayName.isEmpty ? '@$handle' : displayName;
|
||||
|
|
@ -60,6 +67,7 @@ final class const ExploreStudyset({
|
|||
description,
|
||||
frontLanguage,
|
||||
backLanguage,
|
||||
basedOn,
|
||||
license,
|
||||
licenseHolder,
|
||||
licenseYear,
|
||||
|
|
@ -68,6 +76,7 @@ final class const ExploreStudyset({
|
|||
noteCount,
|
||||
paths,
|
||||
pdfs,
|
||||
learners,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -421,6 +421,8 @@ class const _ExploreCard({
|
|||
Text(studyset.title, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(l10n.exploreBy(authorLine(studyset)), style: muted),
|
||||
if (studyset.basedOn case final original?)
|
||||
Text(l10n.exploreBasedOn(original), style: muted),
|
||||
if (description.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
|
|
@ -439,6 +441,8 @@ class const _ExploreCard({
|
|||
style: muted,
|
||||
),
|
||||
ExploreCounts(studyset: studyset),
|
||||
if (studyset.learners case final learners?)
|
||||
Text(l10n.exploreLearners(learners), style: muted),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ class const _Summary({required final ExploreStudyset studyset})
|
|||
if (studyset.secondaryTitle case final secondary?)
|
||||
Text(secondary, style: theme.textTheme.titleSmall),
|
||||
Text(l10n.exploreBy(authorLine(studyset)), style: muted),
|
||||
if (studyset.basedOn case final original?)
|
||||
Text(l10n.exploreBasedOn(original), style: muted),
|
||||
const SizedBox(height: 12),
|
||||
if (studyset.description.isBlank)
|
||||
Text(
|
||||
|
|
@ -72,6 +74,8 @@ class const _Summary({required final ExploreStudyset studyset})
|
|||
style: muted,
|
||||
),
|
||||
ExploreCounts(studyset: studyset),
|
||||
if (studyset.learners case final learners?)
|
||||
Text(l10n.exploreLearners(learners), style: muted),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.exploreLatestRelease(
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ class PublishBloc(
|
|||
ready.next(
|
||||
problem: refused.problem,
|
||||
items: refused.items,
|
||||
license: refused.license,
|
||||
unsettled: refused.problem == PublishProblem.conflicts
|
||||
? await _unsettled(refused.items.toSet(), event.subtree)
|
||||
: const [],
|
||||
|
|
@ -240,15 +241,17 @@ final class const PublishStateReady({
|
|||
final bool publishing = false,
|
||||
final PublishProblem? problem,
|
||||
final List<String> items = const [],
|
||||
final String? license,
|
||||
final List<UnsettledItem> unsettled = const [],
|
||||
final PublishOutcome? outcome,
|
||||
}) extends PublishState {
|
||||
/// This state with [publishing], [problem], [items], [unsettled] and
|
||||
/// [outcome] as given.
|
||||
/// This state with [publishing], [problem], [items], [license],
|
||||
/// [unsettled] and [outcome] as given.
|
||||
PublishStateReady next({
|
||||
bool publishing = false,
|
||||
PublishProblem? problem,
|
||||
List<String> items = const [],
|
||||
String? license,
|
||||
List<UnsettledItem> unsettled = const [],
|
||||
PublishOutcome? outcome,
|
||||
}) => PublishStateReady(
|
||||
|
|
@ -257,6 +260,7 @@ final class const PublishStateReady({
|
|||
publishing: publishing,
|
||||
problem: problem,
|
||||
items: items,
|
||||
license: license,
|
||||
unsettled: unsettled,
|
||||
outcome: outcome,
|
||||
);
|
||||
|
|
@ -268,6 +272,7 @@ final class const PublishStateReady({
|
|||
publishing,
|
||||
problem,
|
||||
items,
|
||||
license,
|
||||
unsettled,
|
||||
outcome,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class ServerpodPublishGateway(final protocol.Client _client)
|
|||
for (final item in error.items ?? const <protocol.UuidValue>[])
|
||||
item.uuid,
|
||||
],
|
||||
license: error.license,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,5 +68,6 @@ class ServerpodPublishGateway(final protocol.Client _client)
|
|||
),
|
||||
],
|
||||
withdrawn: publication.withdrawn,
|
||||
learners: publication.learners,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ final class const Publication({
|
|||
/// Whether its author took it off Explore: nobody can add it, and copies
|
||||
/// get no updates, until it is published again.
|
||||
final bool withdrawn = false,
|
||||
|
||||
/// How many people learn it here, rounded as Explore shows it; null for
|
||||
/// fewer than 5.
|
||||
final int? learners,
|
||||
}) with Equatable {
|
||||
/// The newest release; null before the first.
|
||||
Release? get latest => releases.firstOrNull;
|
||||
|
|
@ -27,7 +31,14 @@ final class const Publication({
|
|||
int get nextVersion => (latest?.version ?? 0) + 1;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [publicId, slug, location, releases, withdrawn];
|
||||
List<Object?> get props => [
|
||||
publicId,
|
||||
slug,
|
||||
location,
|
||||
releases,
|
||||
withdrawn,
|
||||
learners,
|
||||
];
|
||||
}
|
||||
|
||||
/// One release of a published studyset: what it holds, never changed.
|
||||
|
|
@ -99,13 +110,23 @@ enum PublishProblem() {
|
|||
|
||||
/// The studyset is being published already.
|
||||
busy,
|
||||
|
||||
/// A copy of a published studyset whose licence forbids passing on
|
||||
/// adaptations: only its author publishes it.
|
||||
forkForbidden,
|
||||
|
||||
/// A copy of a published studyset under a licence that asks adaptations
|
||||
/// to stay under it ([PublishRefused.license]).
|
||||
licenseMustStay,
|
||||
}
|
||||
|
||||
/// The instance refused to publish, for [problem]; [items] names the
|
||||
/// studysets, or the cards, notes and steps, it is about.
|
||||
/// studysets, or the cards, notes and steps, it is about, and [license] the
|
||||
/// licence a copy must keep.
|
||||
final class PublishRefused(
|
||||
final PublishProblem problem, {
|
||||
final List<String> items = const [],
|
||||
final String? license,
|
||||
}) implements Exception {
|
||||
@override
|
||||
String toString() => 'PublishRefused($problem, $items)';
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ class const _Form({
|
|||
final problem? when about.contains(problem) => _problemText(
|
||||
l10n,
|
||||
problem,
|
||||
license: state.license,
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
|
|
@ -198,6 +199,14 @@ class const _Form({
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_Status(publication: publication),
|
||||
if (publication != null && !publication.withdrawn)
|
||||
Text(
|
||||
switch (publication.learners) {
|
||||
final learners? => l10n.exploreLearners(learners),
|
||||
null => l10n.publishLearnersFew,
|
||||
},
|
||||
style: muted,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.publishIntro, style: muted),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -269,6 +278,8 @@ class const _Form({
|
|||
PublishProblem.nothingChanged,
|
||||
PublishProblem.busy,
|
||||
PublishProblem.noProfile,
|
||||
PublishProblem.forkForbidden,
|
||||
PublishProblem.licenseMustStay,
|
||||
})
|
||||
case final text?) ...[
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -507,8 +518,11 @@ String _date(BuildContext context, DateTime at) => DateFormat.yMMMd(
|
|||
AppLocalizations.of(context).localeName,
|
||||
).format(at.toLocal());
|
||||
|
||||
String _problemText(AppLocalizations l10n, PublishProblem problem) =>
|
||||
switch (problem) {
|
||||
String _problemText(
|
||||
AppLocalizations l10n,
|
||||
PublishProblem problem, {
|
||||
String? license,
|
||||
}) => switch (problem) {
|
||||
PublishProblem.noProfile => l10n.publishNoProfile,
|
||||
PublishProblem.licenseNotOpen => l10n.publishLicenceClosed,
|
||||
PublishProblem.conflicts => l10n.publishConflictsElsewhere,
|
||||
|
|
@ -519,4 +533,8 @@ String _problemText(AppLocalizations l10n, PublishProblem problem) =>
|
|||
PublishProblem.slugTaken => l10n.publishSlugTaken,
|
||||
PublishProblem.slugFixed => l10n.publishNameFixed,
|
||||
PublishProblem.busy => l10n.publishBusy,
|
||||
};
|
||||
PublishProblem.forkForbidden => l10n.publishForkForbidden,
|
||||
PublishProblem.licenseMustStay => l10n.publishLicenseMustStay(
|
||||
license ?? '',
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
|
||||
/// Whether the person's learning counts in Explore (`countLearning` of
|
||||
/// their settings), as their settings hold it now; null until read.
|
||||
class PrivacySettingsBloc(final SettingsRepository _settings)
|
||||
extends Bloc<PrivacySettingsEvent, bool?> {
|
||||
this : super(null) {
|
||||
on<PrivacySettingsEventStarted>(
|
||||
(event, emit) => emit.forEach(
|
||||
_settings.watch(),
|
||||
onData: (settings) => settings.countLearning,
|
||||
),
|
||||
);
|
||||
on<PrivacySettingsEventCountingChosen>(
|
||||
(event, emit) => _settings.update(
|
||||
(settings) => settings.copyWith(countLearning: event.counted),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens to the person's privacy settings.
|
||||
sealed class const PrivacySettingsEvent();
|
||||
|
||||
/// The settings show: follow them.
|
||||
final class const PrivacySettingsEventStarted() extends PrivacySettingsEvent;
|
||||
|
||||
/// The person chose whether their learning counts in Explore.
|
||||
final class const PrivacySettingsEventCountingChosen(final bool counted)
|
||||
extends PrivacySettingsEvent;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue