feat: files in an order of one's own, with Reorder beside the tabs
A PDF now has a position among its studyset's files, as notes and paths have: a fractional index in a column of its own (so moving it on one device and renaming it on another don't overwrite each other), last writer wins. The migration adds it to pdf_source on the server and on each device with 'a0', so PDFs from before stand in the order they were added (ties sort by id), and new ones go at the end. The Files tab lists them in that order by default (Custom order, first in the Sort menu; newest first was the default before). With two files or more, Reorder beside the tabs shows the custom order whatever they were sorted by, a grip in place of each file's options and the Sort menu put away, until Done. The next release changes synced tables anyway (card.buriedAt, since v0.2.2), so older apps have to update for that already. Also: card.position's doc no longer speaks of moving cards. 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
04cbf00e3c
commit
7975586994
8 changed files with 21780 additions and 44 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "alterTable",
|
||||
"alterTable": {
|
||||
"__className__": "serverpod.TableMigration",
|
||||
"name": "pdf_source",
|
||||
"schema": "public",
|
||||
"addColumns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "position",
|
||||
"fieldName": "position",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"columnDefault": "'a0'",
|
||||
"dartType": "String"
|
||||
}
|
||||
],
|
||||
"deleteColumns": [],
|
||||
"modifyColumns": [],
|
||||
"addIndexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "pdf_source_studyset_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "studysetId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "position"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"deleteIndexes": [
|
||||
"pdf_source_studyset_idx"
|
||||
],
|
||||
"addForeignKeys": [],
|
||||
"deleteForeignKeys": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ part '20261002191122122-notes/migration.dart';
|
|||
part '20261002211346701-learning-paths/migration.dart';
|
||||
part '20261003110855698-pdf-sources/migration.dart';
|
||||
part '20261007205440868-card-buried-at/migration.dart';
|
||||
part '20261008165051407-pdf-position/migration.dart';
|
||||
|
||||
/// Migration registry for the client-side database.
|
||||
class MigrationRegistry {
|
||||
|
|
@ -26,6 +27,7 @@ class MigrationRegistry {
|
|||
_Migration20261002211346701_learning_paths(),
|
||||
_Migration20261003110855698_pdf_sources(),
|
||||
_Migration20261007205440868_card_buried_at(),
|
||||
_Migration20261008165051407_pdf_position(),
|
||||
];
|
||||
|
||||
/// List of all client-side database migration versions.
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ abstract class Card
|
|||
/// The studyset the card belongs to; the card goes with it.
|
||||
_ie5k1jc2.Studyset? studyset;
|
||||
|
||||
/// Where the card stands in its studyset: a fractional index, so a card can
|
||||
/// move between two others without renumbering the rest; last writer wins
|
||||
/// (decided 2026-09-30).
|
||||
/// Where the card stands in its studyset: a fractional index (decided
|
||||
/// 2026-09-30), set when the card is created or imported, the order new cards
|
||||
/// and cramming follow. Cards aren't reordered by hand (decided 2026-10-08).
|
||||
String position;
|
||||
|
||||
/// When the card was buried (set aside), in UTC; null while it isn't. It
|
||||
|
|
@ -257,9 +257,9 @@ class CardTable extends _isd.Table<_isc.UuidValue?> {
|
|||
/// The studyset the card belongs to; the card goes with it.
|
||||
_ie5k1jc2.StudysetTable? _studyset;
|
||||
|
||||
/// Where the card stands in its studyset: a fractional index, so a card can
|
||||
/// move between two others without renumbering the rest; last writer wins
|
||||
/// (decided 2026-09-30).
|
||||
/// Where the card stands in its studyset: a fractional index (decided
|
||||
/// 2026-09-30), set when the card is created or imported, the order new cards
|
||||
/// and cramming follow. Cards aren't reordered by hand (decided 2026-10-08).
|
||||
late final _isd.ColumnString position;
|
||||
|
||||
/// When the card was buried (set aside), in UTC; null while it isn't. It
|
||||
|
|
|
|||
|
|
@ -31,16 +31,18 @@ abstract class PdfSource
|
|||
this.spaceId,
|
||||
required this.studysetId,
|
||||
this.studyset,
|
||||
String? position,
|
||||
this.keyId,
|
||||
required this.payload,
|
||||
this.nonce,
|
||||
});
|
||||
}) : position = position ?? 'a0';
|
||||
|
||||
factory PdfSource({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
required _isc.UuidValue studysetId,
|
||||
_ie5k1jc2.Studyset? studyset,
|
||||
String? position,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
|
|
@ -60,6 +62,7 @@ abstract class PdfSource
|
|||
: _itx5j3f5.Protocol().deserialize<_ie5k1jc2.Studyset>(
|
||||
jsonSerialization['studyset'],
|
||||
),
|
||||
position: jsonSerialization['position'] as String?,
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
payload: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['payload'],
|
||||
|
|
@ -85,6 +88,13 @@ abstract class PdfSource
|
|||
/// The studyset the PDF belongs to; the PDF goes with it.
|
||||
_ie5k1jc2.Studyset? studyset;
|
||||
|
||||
/// Where the PDF stands among its studyset's files, as the person put them
|
||||
/// with Reorder: a fractional index, like a note's; last writer wins (decided
|
||||
/// 2026-10-08). A column of its own, so that moving it on one device and
|
||||
/// renaming it on another don't overwrite each other. PDFs from before this
|
||||
/// column all start at 'a0'; ties sort by id, the order they were added in.
|
||||
String position;
|
||||
|
||||
/// The encryption key's id; always null (ADR 0002, rule 6).
|
||||
String? keyId;
|
||||
|
||||
|
|
@ -107,6 +117,7 @@ abstract class PdfSource
|
|||
int? spaceId,
|
||||
_isc.UuidValue? studysetId,
|
||||
_ie5k1jc2.Studyset? studyset,
|
||||
String? position,
|
||||
String? keyId,
|
||||
_idt.ByteData? payload,
|
||||
_idt.ByteData? nonce,
|
||||
|
|
@ -119,6 +130,7 @@ abstract class PdfSource
|
|||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'studysetId': studysetId.toJson(),
|
||||
if (studyset != null) 'studyset': studyset?.toJson(),
|
||||
'position': position,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
|
|
@ -133,6 +145,7 @@ abstract class PdfSource
|
|||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'studysetId': studysetId.toJson(),
|
||||
if (studyset != null) 'studyset': studyset?.toJsonForProtocol(),
|
||||
'position': position,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
|
|
@ -175,6 +188,7 @@ class _PdfSourceImpl extends PdfSource {
|
|||
int? spaceId,
|
||||
required _isc.UuidValue studysetId,
|
||||
_ie5k1jc2.Studyset? studyset,
|
||||
String? position,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
|
|
@ -183,6 +197,7 @@ class _PdfSourceImpl extends PdfSource {
|
|||
spaceId: spaceId,
|
||||
studysetId: studysetId,
|
||||
studyset: studyset,
|
||||
position: position,
|
||||
keyId: keyId,
|
||||
payload: payload,
|
||||
nonce: nonce,
|
||||
|
|
@ -197,6 +212,7 @@ class _PdfSourceImpl extends PdfSource {
|
|||
Object? spaceId = _Undefined,
|
||||
_isc.UuidValue? studysetId,
|
||||
Object? studyset = _Undefined,
|
||||
String? position,
|
||||
Object? keyId = _Undefined,
|
||||
_idt.ByteData? payload,
|
||||
Object? nonce = _Undefined,
|
||||
|
|
@ -208,6 +224,7 @@ class _PdfSourceImpl extends PdfSource {
|
|||
studyset: studyset is _ie5k1jc2.Studyset?
|
||||
? studyset
|
||||
: this.studyset?.copyWith(),
|
||||
position: position ?? this.position,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
payload: payload ?? this.payload.clone(),
|
||||
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
|
||||
|
|
@ -230,6 +247,11 @@ class PdfSourceUpdateTable extends _isd.UpdateTable<PdfSourceTable> {
|
|||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<String, String> position(String value) => _isd.ColumnValue(
|
||||
table.position,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
|
||||
table.keyId,
|
||||
value,
|
||||
|
|
@ -259,6 +281,11 @@ class PdfSourceTable extends _isd.Table<_isc.UuidValue?> {
|
|||
'studysetId',
|
||||
this,
|
||||
);
|
||||
position = _isd.ColumnString(
|
||||
'position',
|
||||
this,
|
||||
hasDefault: true,
|
||||
);
|
||||
keyId = _isd.ColumnString(
|
||||
'keyId',
|
||||
this,
|
||||
|
|
@ -283,6 +310,13 @@ class PdfSourceTable extends _isd.Table<_isc.UuidValue?> {
|
|||
/// The studyset the PDF belongs to; the PDF goes with it.
|
||||
_ie5k1jc2.StudysetTable? _studyset;
|
||||
|
||||
/// Where the PDF stands among its studyset's files, as the person put them
|
||||
/// with Reorder: a fractional index, like a note's; last writer wins (decided
|
||||
/// 2026-10-08). A column of its own, so that moving it on one device and
|
||||
/// renaming it on another don't overwrite each other. PDFs from before this
|
||||
/// column all start at 'a0'; ties sort by id, the order they were added in.
|
||||
late final _isd.ColumnString position;
|
||||
|
||||
/// The encryption key's id; always null (ADR 0002, rule 6).
|
||||
late final _isd.ColumnString keyId;
|
||||
|
||||
|
|
@ -312,6 +346,7 @@ class PdfSourceTable extends _isd.Table<_isc.UuidValue?> {
|
|||
id,
|
||||
spaceId,
|
||||
studysetId,
|
||||
position,
|
||||
keyId,
|
||||
payload,
|
||||
nonce,
|
||||
|
|
|
|||
|
|
@ -1471,6 +1471,13 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'position',
|
||||
columnType: _isd.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
columnDefault: '\'a0\'',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'keyId',
|
||||
columnType: _isd.ColumnType.text,
|
||||
|
|
@ -1535,6 +1542,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
type: _isd.IndexElementDefinitionType.column,
|
||||
definition: 'studysetId',
|
||||
),
|
||||
_isd.IndexElementDefinition(
|
||||
type: _isd.IndexElementDefinitionType.column,
|
||||
definition: 'position',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue