feat(learn): Explore by what a licence allows, a skeleton while the library loads, and an assistant's agreement per provider (ADR 0005, rule 12; ADR 0021, D66)

- Explore filters by what a studyset's licence lets a learner do: adapt
  it (no NoDerivatives) and use it commercially (no NonCommercial),
  which the instance reads from the release's licence
  (ExploreQuery.adaptable, commercial) and the app the same way
  (LicenseUse). An instance from before ignores both.
- While the studysets are read, the Library shows shimmering skeletons
  of their cards rather than a spinner: halkyon_ui's new Shimmer and
  SkeletonBox, still where the system asks for no animations.
- The scope's "Include private studysets" is offered only while the
  person has a private studyset.
- An agreement to what goes where is per provider (protocol and host):
  agreeing to one assistant agrees to every assistant of that provider,
  those granted later too, and the agreement sheet says so. No
  migration: it is written on every grant of the provider, so taking
  one back keeps it for the others, and read per provider from any of
  them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-02 01:09:04 +01:00
commit 5641285d31
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 970 additions and 85 deletions

View file

@ -28,7 +28,8 @@ final _language = RegExp(r'^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$');
final _sha256 = RegExp(r'^[0-9a-f]{64}$');
/// Explore (ADR 0005, rule 4): the studysets published on this instance,
/// found by fit (search, languages, kinds of content), never by popularity,
/// found by fit (search, languages, kinds of content, what the licence
/// allows), never by popularity,
/// the newest release first; and their releases, to learn from as a copy.
/// For everyone signed in: published content is public, and what a person
/// takes from it stays theirs alone. A studyset its author withdrew is
@ -81,6 +82,16 @@ class ExploreEndpoint extends Endpoint {
conditions.add('latest."$column" > 0');
}
}
// What a licence allows, as Creative Commons' identifiers say it: only
// theirs forbid adapting (-ND-) or selling (-NC-), any version or port;
// every licence a release may have allows passing it on
// (`allowsPublishing`).
if (query.adaptable) {
conditions.add('upper(latest."license") NOT LIKE \'%-ND-%\'');
}
if (query.commercial) {
conditions.add('upper(latest."license") NOT LIKE \'%-NC-%\'');
}
return await _latest(
session,
where: conditions,

View file

@ -1,7 +1,7 @@
### What Explore is asked for: words to search titles and authors for, the
### languages (a primary language, `ja`, takes its regional ones too), and
### which kinds of content it must hold. Results come newest first, a page
### at a time.
### languages (a primary language, `ja`, takes its regional ones too),
### which kinds of content it must hold, and what its licence must allow.
### Results come newest first, a page at a time.
class: ExploreQuery
fields:
text: String, default=''
@ -11,5 +11,9 @@ fields:
withNotes: bool, default=false
withPaths: bool, default=false
withPdfs: bool, default=false
### Only licences allowing adaptations: no NoDerivatives.
adaptable: bool, default=false
### Only licences allowing commercial use: no NonCommercial.
commercial: bool, default=false
### How many to skip, for the next page.
offset: int, default=0

View file

@ -13,9 +13,9 @@
import 'package:serverpod/serverpod.dart' as _is;
/// What Explore is asked for: words to search titles and authors for, the
/// languages (a primary language, `ja`, takes its regional ones too), and
/// which kinds of content it must hold. Results come newest first, a page
/// at a time.
/// languages (a primary language, `ja`, takes its regional ones too),
/// which kinds of content it must hold, and what its licence must allow.
/// Results come newest first, a page at a time.
abstract class ExploreQuery
implements _is.SerializableModel, _is.ProtocolSerialization {
ExploreQuery._({
@ -26,12 +26,16 @@ abstract class ExploreQuery
bool? withNotes,
bool? withPaths,
bool? withPdfs,
bool? adaptable,
bool? commercial,
int? offset,
}) : text = text ?? '',
withCards = withCards ?? false,
withNotes = withNotes ?? false,
withPaths = withPaths ?? false,
withPdfs = withPdfs ?? false,
adaptable = adaptable ?? false,
commercial = commercial ?? false,
offset = offset ?? 0;
factory ExploreQuery({
@ -42,6 +46,8 @@ abstract class ExploreQuery
bool? withNotes,
bool? withPaths,
bool? withPdfs,
bool? adaptable,
bool? commercial,
int? offset,
}) = _ExploreQueryImpl;
@ -62,6 +68,12 @@ abstract class ExploreQuery
withPdfs: jsonSerialization['withPdfs'] == null
? null
: _is.BoolJsonExtension.fromJson(jsonSerialization['withPdfs']),
adaptable: jsonSerialization['adaptable'] == null
? null
: _is.BoolJsonExtension.fromJson(jsonSerialization['adaptable']),
commercial: jsonSerialization['commercial'] == null
? null
: _is.BoolJsonExtension.fromJson(jsonSerialization['commercial']),
offset: jsonSerialization['offset'] as int?,
);
}
@ -80,6 +92,12 @@ abstract class ExploreQuery
bool withPdfs;
/// Only licences allowing adaptations: no NoDerivatives.
bool adaptable;
/// Only licences allowing commercial use: no NonCommercial.
bool commercial;
/// How many to skip, for the next page.
int offset;
@ -94,6 +112,8 @@ abstract class ExploreQuery
bool? withNotes,
bool? withPaths,
bool? withPdfs,
bool? adaptable,
bool? commercial,
int? offset,
});
@override
@ -107,6 +127,8 @@ abstract class ExploreQuery
'withNotes': withNotes,
'withPaths': withPaths,
'withPdfs': withPdfs,
'adaptable': adaptable,
'commercial': commercial,
'offset': offset,
};
}
@ -122,6 +144,8 @@ abstract class ExploreQuery
'withNotes': withNotes,
'withPaths': withPaths,
'withPdfs': withPdfs,
'adaptable': adaptable,
'commercial': commercial,
'offset': offset,
};
}
@ -143,6 +167,8 @@ class _ExploreQueryImpl extends ExploreQuery {
bool? withNotes,
bool? withPaths,
bool? withPdfs,
bool? adaptable,
bool? commercial,
int? offset,
}) : super._(
text: text,
@ -152,6 +178,8 @@ class _ExploreQueryImpl extends ExploreQuery {
withNotes: withNotes,
withPaths: withPaths,
withPdfs: withPdfs,
adaptable: adaptable,
commercial: commercial,
offset: offset,
);
@ -167,6 +195,8 @@ class _ExploreQueryImpl extends ExploreQuery {
bool? withNotes,
bool? withPaths,
bool? withPdfs,
bool? adaptable,
bool? commercial,
int? offset,
}) {
return ExploreQuery(
@ -179,6 +209,8 @@ class _ExploreQueryImpl extends ExploreQuery {
withNotes: withNotes ?? this.withNotes,
withPaths: withPaths ?? this.withPaths,
withPdfs: withPdfs ?? this.withPdfs,
adaptable: adaptable ?? this.adaptable,
commercial: commercial ?? this.commercial,
offset: offset ?? this.offset,
);
}