Skip to content

Repository files navigation

liblanis

pub package

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)
  • sharedOverAccountSettings and accountSpecificSettings with 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.

About

This dart package provides parsing for the school portal hesse.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages