liblanis 0.1.2
liblanis: ^0.1.2 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.flutter-io.cn. 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.flutter-io.cn 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.