feat(library): who made a studyset, and whether it is public

Each card's foot now says who made the studyset: "By you", or for a copy
of a published studyset its author's handle. Beside the star, a lock or a
globe says whether it is out on Explore, and opens the Publish sheet.

The instance says which of the person's studysets are published and not
withdrawn (`PublishEndpoint.published`, additive: older apps don't call
it), and the app remembers that in the person's folder, so the library
shows it offline too; publishing, withdrawing and looking one up keep it
current.

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:
Tizian 「ティツィアーン」 2026-10-10 05:17:10 +02:00
commit bc3e8d9ac3
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 729 additions and 51 deletions

View file

@ -921,6 +921,16 @@ class Endpoints extends _is.EndpointDispatch {
params['studysetId'],
),
),
'published': _is.MethodConnector(
name: 'published',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['publish'] as _iyiav298.PublishEndpoint)
.published(session),
),
'publish': _is.MethodConnector(
name: 'publish',
params: {

View file

@ -47,5 +47,6 @@ profiles:
- byHandle:
publish:
- publication:
- published:
- publish:
- withdraw:

View file

@ -64,6 +64,19 @@ class PublishEndpoint extends Endpoint {
return published == null ? null : await _publicationOf(session, published);
}
/// Your studysets out on Explore: published and not withdrawn, which the
/// library marks as public.
Future<List<UuidValue>> published(Session session) async {
final person = session.authenticated!.authUserId;
// SELECT … FROM "published_studyset"
// WHERE "authUserId" = $1 AND "withdrawnAt" IS NULL
final published = await PublishedStudyset.db.find(
session,
where: (t) => t.authUserId.equals(person) & t.withdrawnAt.equals(null),
);
return [for (final row in published) row.studysetId];
}
/// Publishes your studyset [studysetId] with everything inside it as its
/// next release, [notes] saying what changed; at the first, under the
/// name [slug] in its location, which stays its name from then on.

View file

@ -861,6 +861,24 @@ void main() {
},
);
test("the studysets out on Explore are the person's published ones "
'not withdrawn', () async {
await profile(alice, 'alice');
await profile(bob, 'bob');
final alices = await japanese(alice);
final bobs = await japanese(bob);
expect(await endpoints.publish.published(signedIn(alice)), isEmpty);
await publish(alice, alices.root);
await publish(bob, bobs.root);
await endpoints.publish.withdraw(signedIn(bob), bobs.root);
expect(await endpoints.publish.published(signedIn(alice)), [
alices.root,
]);
expect(await endpoints.publish.published(signedIn(bob)), isEmpty);
});
test('deleting a person deletes their published studysets and their '
'repositories', () async {
useServerAuth();

View file

@ -1571,6 +1571,36 @@ class _PublishEndpoint {
});
}
_ida.Future<List<_is.UuidValue>> published(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'publish',
method: 'published',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'publish',
methodName: 'published',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_is.UuidValue>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_ieaj397n.Publication> publish(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue studysetId,