liblanis 0.1.1
liblanis: ^0.1.1 copied to clipboard
Build apps and scripts against SPH without Flutter - batteries included.
liblanis #
Pure Dart client for Schulportal Hessen (SPH / Lanis).
Session stack, multi-account storage, optional sqlite3 persistence, injectable secrets, and applet parsers. No Flutter dependency for the core API. Flutter apps can use the Riverpod integration documented below.
Published on pub.dev. Also used as a submodule of lanis-mobile/lanis.
Features #
- Full SPH session stack (login, cookies, RSA/AES cryptor, HTML/JSON applet parsers)
**EasyLanisClient**— imperative API without Riverpod (default for scripts, CI, other Dart projects)- Optional Riverpod multi-account registry for Flutter apps
- Optional single SQLite database (file or in-memory)
sharedOverAccountSettingsandaccountSpecificSettingswith typed helpers- Offline applet snapshots table (substitutions & timetable write in v1; API is generic)
- Configurable
StorageManager(caller supplies cache directory)
Install #
dart pub add liblanis
Or add liblanis under dependencies in your pubspec.yaml (see pub.dev for the current version).
For Flutter apps that use the Riverpod API, also add:
dependencies:
flutter_riverpod: ^3.0.0
sqlite3_flutter_libs: ^0.5.0 # load native sqlite on mobile
Quick start #
Import package:liblanis/easy_client.dart. No ProviderScope or code generation required.
In-memory (tests, quick scripts) #
import 'package:liblanis/easy_client.dart';
Future<void> main() async {
final client = EasyLanisClient.inMemory();
final id = await client.accounts.add(
schoolId: 5151,
username: 'student.user',
password: r'...',
);
await client.login(accountId: id);
// Direct parser call
final plan = await client.parsers.substitutions.getHome();
// Cached fetch with offline fallback (same as AppletParser.fetchData)
final response = await client.parsers.calendar.fetch(forceRefresh: true);
if (response.contentStatus == ContentStatus.offline) {
// using cached events
}
client.connection.onStatusChanged.listen((status) {
// ConnectionStatus.connected / disconnected
});
await client.logout();
await client.dispose();
}
Available parsers #
Access via client.parsers after login:
| Getter | Applet |
|---|---|
substitutions |
Vertretungsplan |
timetable |
Stundenplan (student) |
calendar |
Kalender |
conversations |
Nachrichten |
lessonsStudent |
Mein Unterricht (student) |
lessonsTeacher |
Mein Unterricht (teacher) |
dataStorage |
Dateispeicher |
studyGroups |
Lerngruppen |
Generic access for smoke tests: client.parsers.forApplet('kalender.php').
Flutter (Riverpod) #
Prefer package:liblanis/liblanis.dart when you want the provider-based multi-account API used by lanis-mobile.
On Flutter mobile hosts, initialize sqlite before runApp:
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
// ...
}
Configure and mount #
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:liblanis/liblanis.dart';
Future<void> main() async {
final overrides = LanisClient.configure(
databasePath: '/path/to/lanis.db', // null => in-memory
secretStore: MySecureStore(), // required when databasePath != null
documentCacheDirectory: '/path/to/cache', // required for downloads
// httpAdapter: cronetAdapter, // optional
userAgent: 'Lanis-Mobile/v3.7.2+83',
);
runApp(
ProviderScope(
overrides: overrides,
child: const MyApp(),
),
);
}
class MySecureStore implements SecretStore {
// e.g. FlutterSecureStorage
@override
Future<void> write(String key, String value) async { /* ... */ }
@override
Future<String?> read(String key) async => null;
@override
Future<void> delete(String key) async { /* ... */ }
}
Accounts & session #
final id = await ref.read(accountsProvider.notifier).add(
schoolId: 5151,
schoolName: 'Example',
username: 'user',
password: 'pass',
);
await ref.read(activeAccountProvider.notifier).select(id);
await ref.read(sessionProvider.notifier).authenticate();
ref.listen(connectionStatusProvider, (prev, next) {
// ConnectionStatus.connected / disconnected
});
Settings #
final shared = ref.read(sharedOverAccountSettingsProvider);
shared.setString('theme', 'dark');
final account = ref.read(accountSpecificSettingsProvider);
account.setJsonMap('vertretungsplan.php/filter', {/* ... */});
Applets #
final response = await ref.read(substitutionsProvider.future);
if (response.contentStatus == ContentStatus.offline) {
// cached SubstitutionPlan
}
final plan = response.content;
// Or use the long-lived parser + stream:
final parser = ref.read(substitutionsParserProvider);
await parser.fetchData();
parser.stream.listen(/* FetcherResponse<SubstitutionPlan> */);
Document downloads #
final storage = ref.read(storageManagerProvider);
if (storage != null) {
final path = await storage.downloadFile(url, filename);
}
Calling download APIs without documentCacheDirectory throws StorageNotConfiguredException.
Configuration rules #
| Option | Behavior |
|---|---|
databasePath: null |
In-memory sqlite; SecretStore optional |
databasePath: set |
File DB; **secretStore required** |
documentCacheDirectory: null |
Downloads disabled |
httpAdapter |
Optional Dio adapter injection |
Development #
cd liblanis
dart pub get
dart run build_runner build
dart test
dart analyze
License #
GPL-3.0 — see LICENSE.