diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51910db74..c27cd7fbb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,12 @@ jobs: os: macos-latest - target: ios os: macos-latest + - target: doudou-server-linux + os: ubuntu-latest + - target: doudou-server-windows + os: windows-latest + - target: doudou-server-macos + os: macos-latest steps: - uses: actions/checkout@v4 @@ -63,7 +69,7 @@ jobs: run: flutter doctor -v - name: Install Linux build dependencies - if: matrix.target == 'linux' + if: matrix.target == 'linux' || matrix.target == 'doudou-server-linux' run: | sudo apt-get update -y sudo apt-get install -y \ @@ -77,6 +83,13 @@ jobs: - name: Install dependencies run: flutter pub get + - name: Install doudou-server dependencies + if: startsWith(matrix.target, 'doudou-server-') + working-directory: packages/doudou_server + run: | + flutter pub get + dart run build_runner build --delete-conflicting-outputs + - name: Build debug + release shell: bash run: | @@ -145,6 +158,24 @@ jobs: flutter build ios --release --no-codesign (cd build/ios/iphoneos && zip -ry "$OLDPWD/artifacts/doudou-ios-release-${suffix}.zip" ./*.app) ;; + doudou-server-linux) + (cd packages/doudou_server && flutter build linux --debug) + tar -czf "artifacts/doudou-server-linux-debug-${suffix}.tar.gz" -C packages/doudou_server/build/linux/x64/debug/bundle . + (cd packages/doudou_server && flutter build linux --release) + tar -czf "artifacts/doudou-server-linux-release-${suffix}.tar.gz" -C packages/doudou_server/build/linux/x64/release/bundle . + ;; + doudou-server-windows) + (cd packages/doudou_server && flutter build windows --debug) + (cd packages/doudou_server/build/windows/x64/runner/Debug && 7z a "$OLDPWD/artifacts/doudou-server-windows-debug-${suffix}.zip" "*") + (cd packages/doudou_server && flutter build windows --release) + (cd packages/doudou_server/build/windows/x64/runner/Release && 7z a "$OLDPWD/artifacts/doudou-server-windows-release-${suffix}.zip" "*") + ;; + doudou-server-macos) + (cd packages/doudou_server && flutter build macos --debug) + (cd packages/doudou_server/build/macos/Build/Products/Debug && zip -ry "$OLDPWD/artifacts/doudou-server-macos-debug-${suffix}.zip" ./*.app) + (cd packages/doudou_server && flutter build macos --release) + (cd packages/doudou_server/build/macos/Build/Products/Release && zip -ry "$OLDPWD/artifacts/doudou-server-macos-release-${suffix}.zip" ./*.app) + ;; *) echo "Unknown target: ${{ matrix.target }}" >&2 exit 1 diff --git a/lib/main.dart b/lib/main.dart index 55b8c3091..89d50c6d6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import '/l10n/app_localizations.dart'; import '/ui/screens/Search/search_screen_controller.dart'; import '/services/downloader.dart'; import '/services/library_sync_service.dart'; +import '/services/doudou_server_sync_service.dart'; import '/services/piped_service.dart'; import '/services/playback_diagnostics_service.dart'; import 'utils/app_link_controller.dart'; @@ -146,6 +147,9 @@ Future startApplicationServices() async { Get.lazyPut(() => SettingsScreenController(), fenix: true); Get.lazyPut(() => Downloader(), fenix: true); Get.lazyPut(() => LibrarySyncService(), fenix: true); + // Eager registration so onInit runs at launch and the periodic sync starts + // immediately, even before the settings screen is opened. + Get.put(DoudouServerSyncService(), permanent: true); Get.lazyPut(() => SearchScreenController(), fenix: true); Get.lazyPut(() => PlaybackDiagnosticsService(), fenix: true); if (GetPlatform.isAndroid) { diff --git a/lib/models/doudou_server.dart b/lib/models/doudou_server.dart new file mode 100644 index 000000000..92771e16d --- /dev/null +++ b/lib/models/doudou_server.dart @@ -0,0 +1,42 @@ +/// Configuration for a doudou-server instance. Stored locally on each client. +/// +/// The [key] is the shared password the server operator set via +/// `doudou-server -set login`. It is stored locally so the client can +/// authenticate, but it is never sent to any music server and never leaves the +/// client except when talking to this doudou-server. +class DoudouServerConfig { + DoudouServerConfig({ + required this.url, + required this.key, + this.deviceName, + }); + + /// Base URL of the doudou-server, e.g. `http://192.168.1.20:7427`. + final String url; + final String key; + final String? deviceName; + + Map toMap() => { + 'url': url, + 'key': key, + 'deviceName': deviceName, + }; + + factory DoudouServerConfig.fromMap(Map map) => + DoudouServerConfig( + url: map['url'] as String? ?? '', + key: map['key'] as String? ?? '', + deviceName: map['deviceName'] as String?, + ); + + DoudouServerConfig copyWith({ + String? url, + String? key, + String? deviceName, + }) => + DoudouServerConfig( + url: url ?? this.url, + key: key ?? this.key, + deviceName: deviceName ?? this.deviceName, + ); +} diff --git a/lib/services/doudou_server_client.dart b/lib/services/doudou_server_client.dart new file mode 100644 index 000000000..ee3bc86ae --- /dev/null +++ b/lib/services/doudou_server_client.dart @@ -0,0 +1,154 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; + +import '/models/doudou_server.dart'; + +/// One music server entry as mirrored from doudou-server. Same shape the +/// server stores, minus credentials. +class RemoteMusicServer { + RemoteMusicServer({ + required this.id, + required this.name, + required this.type, + required this.url, + this.isDefault = false, + }); + + final String id; + final String name; + final String type; + final String url; + final bool isDefault; + + factory RemoteMusicServer.fromJson(Map json) => + RemoteMusicServer( + id: json['id'] as String, + name: json['name'] as String, + type: json['type'] as String, + url: json['url'] as String, + isDefault: json['isDefault'] as bool? ?? false, + ); +} + +class RemoteSnapshot { + RemoteSnapshot({ + required this.musicServerId, + required this.kind, + required this.version, + required this.updatedAtMs, + required this.payload, + }); + + final String musicServerId; + final String kind; + final int version; + final int updatedAtMs; + final String payload; + + factory RemoteSnapshot.fromJson(Map json) => RemoteSnapshot( + musicServerId: json['musicServerId'] as String, + kind: json['kind'] as String, + version: json['version'] as int? ?? 0, + updatedAtMs: json['updatedAtMs'] as int? ?? 0, + payload: json['payload'] as String? ?? '[]', + ); +} + +/// Thin HTTP client for the doudou-server REST API. +class DoudouServerClient { + DoudouServerClient(this._config) : _dio = Dio(BaseOptions( + baseUrl: _config.url.replaceAll(RegExp(r'/+$'), ''), + connectTimeout: const Duration(seconds: 8), + receiveTimeout: const Duration(seconds: 30), + headers: {'X-Doudou-Key': _config.key}, + )); + + final DoudouServerConfig _config; + final Dio _dio; + + DoudouServerConfig get config => _config; + + Future health() async { + try { + final res = await _dio.get('/api/v1/health'); + return res.statusCode == 200 && + (res.data as Map)['status'] == 'ok'; + } catch (_) { + return false; + } + } + + Future> listServers() async { + final res = await _dio.get('/api/v1/servers'); + final list = res.data as List; + return list + .map((e) => RemoteMusicServer.fromJson(e as Map)) + .toList(); + } + + Future upsertServer(RemoteMusicServer server) async { + await _dio.put('/api/v1/servers', data: { + 'id': server.id, + 'name': server.name, + 'type': server.type, + 'url': server.url, + 'isDefault': server.isDefault, + }); + } + + Future deleteServer(String id) async { + await _dio.delete('/api/v1/servers/$id'); + } + + Future> listSnapshots(String musicServerId) async { + final res = await _dio.get('/api/v1/snapshots', + queryParameters: {'musicServerId': musicServerId}); + final list = res.data as List; + return list + .map((e) => RemoteSnapshot.fromJson(e as Map)) + .toList(); + } + + /// Pushes a library snapshot. Returns true if the server accepted it. + Future pushSnapshot({ + required String musicServerId, + required String kind, + required int version, + required String payload, + }) async { + final res = await _dio.put('/api/v1/snapshots', data: { + 'musicServerId': musicServerId, + 'kind': kind, + 'version': version, + 'payload': payload, + }); + final data = res.data as Map; + return data['accepted'] == true; + } + + Future registerClient(String clientId, String name) async { + await _dio.post('/api/v1/clients/register', data: { + 'id': clientId, + 'name': name, + }); + } + + void close() => _dio.close(); +} + +/// Encodes a list of maps into the JSON string the server expects as a +/// snapshot payload. +String encodeSnapshotPayload(List> items) => + jsonEncode(items); + +List> decodeSnapshotPayload(String payload) { + if (payload.isEmpty) return []; + final decoded = jsonDecode(payload); + if (decoded is List) { + return decoded + .map((e) => e is Map ? e : Map.from(e as Map)) + .toList(); + } + return []; +} diff --git a/lib/services/doudou_server_sync_service.dart b/lib/services/doudou_server_sync_service.dart new file mode 100644 index 000000000..c47c75177 --- /dev/null +++ b/lib/services/doudou_server_sync_service.dart @@ -0,0 +1,417 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:uuid/uuid.dart'; + +import '/models/doudou_server.dart'; +import '/models/server.dart'; +import '/services/doudou_server_client.dart'; +import '/services/library_sync_service.dart'; +import '/ui/screens/Settings/settings_screen_controller.dart'; +import '/utils/server_storage.dart'; + +/// Syncs library snapshots and music server URLs between this doudou client +/// and a configured [DoudouServerConfig]. +/// +/// The doudou-server is the source of truth for library snapshots. After the +/// local [LibrarySyncService] pulls a fresh library from a music server, this +/// service pushes the resulting cache to doudou-server. Other devices pull +/// from doudou-server into their local cache so they see the same library +/// without having to talk to the music server themselves. +/// +/// Music server URLs are mirrored both ways: local servers are pushed up so +/// other devices know about them, and remote servers are pulled down so this +/// device can use them. Credentials never travel through doudou-server, only +/// URLs and display metadata. +/// +/// The shared key is stored via flutter_secure_storage (OS keychain/keystore) +/// rather than plaintext Hive, so a compromised Hive box doesn't leak the +/// doudou-server password. +class DoudouServerSyncService extends GetxService { + DoudouServerSyncService(); + + static const Duration _syncInterval = Duration(minutes: 30); + static const _secureKeyKey = 'doudou_server_key'; + + final isSyncing = false.obs; + final lastSyncTimeMs = RxnInt(); + final lastError = ''.obs; + final isConfigured = false.obs; + final config = Rxn(); + + /// remoteIds of music servers currently being pushed/pulled this round. + final syncingServerIds = {}.obs; + + /// remoteIds of music servers that have completed at least one successful + /// sync round with doudou-server. Persisted across restarts. + final syncedServerIds = {}.obs; + + Timer? _timer; + DoudouServerClient? _client; + String? _clientId; + final _secureStorage = const FlutterSecureStorage(); + + @override + void onInit() { + super.onInit(); + _loadConfig(); + _loadSyncedServerIds(); + _startPeriodicSync(); + } + + @override + void onClose() { + _timer?.cancel(); + _client?.close(); + super.onClose(); + } + + // -- config ------------------------------------------------------------ + + Future setConfig(DoudouServerConfig? newConfig) async { + _client?.close(); + _client = null; + if (newConfig == null) { + config.value = null; + isConfigured.value = false; + syncingServerIds.clear(); + syncedServerIds.clear(); + await _secureStorage.delete(key: _secureKeyKey); + final box = Hive.box('AppPrefs'); + await box.delete('doudouServer'); + await box.delete('doudouSyncedServerIds'); + } else { + // Persist non-secret fields in Hive, the shared key in secure storage. + await _secureStorage.write(key: _secureKeyKey, value: newConfig.key); + final box = Hive.box('AppPrefs'); + await box.put('doudouServer', newConfig.copyWith(key: '').toMap()); + config.value = newConfig; + isConfigured.value = true; + _client = DoudouServerClient(newConfig); + unawaited(syncNow()); + } + } + + Future _loadSyncedServerIds() async { + final box = Hive.isBoxOpen('AppPrefs') ? Hive.box('AppPrefs') : null; + if (box == null) return; + final raw = box.get('doudouSyncedServerIds'); + if (raw is List) { + syncedServerIds.addAll(raw.whereType()); + } + } + + Future _persistSyncedServerIds() async { + final box = Hive.isBoxOpen('AppPrefs') ? Hive.box('AppPrefs') : await Hive.openBox('AppPrefs'); + await box.put('doudouSyncedServerIds', syncedServerIds.toList()); + } + + Future _loadConfig() async { + final box = Hive.isBoxOpen('AppPrefs') ? Hive.box('AppPrefs') : null; + if (box == null) return; + final raw = box.get('doudouServer'); + if (raw is! Map) return; + final key = await _secureStorage.read(key: _secureKeyKey); + if (key == null || key.isEmpty) return; + final loaded = DoudouServerConfig.fromMap(Map.from(raw)); + _config = loaded.copyWith(key: key); + config.value = _config; + isConfigured.value = true; + _client = DoudouServerClient(_config!); + } + + DoudouServerConfig? _config; + + // -- sync -------------------------------------------------------------- + + void _startPeriodicSync() { + _timer?.cancel(); + _timer = Timer.periodic(_syncInterval, (_) => syncNow()); + } + + Future onAppResumed() => syncNow(); + + /// Syncs a single music server with doudou-server. Used by the add/edit + /// server dialog's "Sync with doudou-server" button. + Future syncServer(SettingsServer server) async { + final client = _client; + if (client == null) return; + final remoteId = remoteIdFor(server.type, server.serverUrl); + syncingServerIds.add(remoteId); + try { + // Push this server's URL up so other devices can see it. + if (!server.isDefault) { + try { + await client.upsertServer(RemoteMusicServer( + id: remoteId, + name: server.name, + type: server.type.name, + url: server.serverUrl ?? '', + isDefault: false, + )); + } catch (_) {} + } + // Push local cache for this server. + for (final kind in LibraryKind.values) { + try { + final payload = await _readLocalCache(server.id, kind); + if (payload == null) continue; + final version = await _readRevision(remoteId, kind); + await client.pushSnapshot( + musicServerId: remoteId, + kind: kind.name, + version: version, + payload: payload, + ); + } catch (_) {} + } + // Pull remote snapshots for this server. + try { + final remoteSnapshots = await client.listSnapshots(remoteId); + for (final snap in remoteSnapshots) { + final kind = _matchKind(snap.kind); + if (kind == null) continue; + final localVersion = await _readRevision(remoteId, kind); + if (snap.version <= localVersion) continue; + await _writeLocalCache(server.id, kind, snap.payload); + await _writeRevision(remoteId, kind, snap.version); + } + } catch (_) {} + syncedServerIds.add(remoteId); + } finally { + syncingServerIds.remove(remoteId); + await _persistSyncedServerIds(); + } + } + + Future syncNow() async { + final client = _client; + if (client == null) return; + if (isSyncing.value) return; + + isSyncing.value = true; + lastError.value = ''; + try { + await _ensureRegistered(client); + await _syncServerUrls(client); + await _pushLocalLibraries(client); + await _pullRemoteLibraries(client); + lastSyncTimeMs.value = DateTime.now().millisecondsSinceEpoch; + } catch (e) { + lastError.value = e.toString(); + } finally { + isSyncing.value = false; + } + } + + Future _ensureRegistered(DoudouServerClient client) async { + final id = await _clientIdForThisDevice(); + final name = _config?.deviceName ?? Platform.localHostname; + try { + await client.registerClient(id, name); + } catch (_) { + // Registration is best-effort; the server may not be reachable yet. + } + } + + Future _clientIdForThisDevice() async { + if (_clientId != null) return _clientId!; + final box = Hive.box('AppPrefs'); + final stored = box.get('doudouClientId'); + if (stored is String && stored.isNotEmpty) { + _clientId = stored; + return stored; + } + final id = 'doudou-${const Uuid().v4()}'; + await box.put('doudouClientId', id); + _clientId = id; + return id; + } + + // -- server urls ------------------------------------------------------- + + Future _syncServerUrls(DoudouServerClient client) async { + final settings = Get.find(); + final localServers = settings.servers.toList(); + + // Push every non-default local server up. Credentials are stripped. + for (final s in localServers) { + if (s.isDefault) continue; + try { + await client.upsertServer(RemoteMusicServer( + id: remoteIdFor(s.type, s.serverUrl), + name: s.name, + type: s.type.name, + url: s.serverUrl ?? '', + isDefault: false, + )); + } catch (_) {} + } + + // Pull remote servers and mirror them locally. We only add servers we + // don't already have by URL. We never overwrite credentials. YouTube + // Music entries have no URL so they're matched by type instead. + final remote = await client.listServers(); + final localUrls = localServers + .where((s) => s.serverUrl != null && s.serverUrl!.isNotEmpty) + .map((s) => s.serverUrl!.toLowerCase()) + .toSet(); + final localTypes = localServers.map((s) => s.type).toSet(); + for (final r in remote) { + final type = _parseServerType(r.type); + if (type == null) continue; + if (r.url.isEmpty) { + // YTM-style entry with no URL. Skip if we already have this type. + if (localTypes.contains(type)) continue; + settings.addServerWithCredentials(type); + } else { + if (localUrls.contains(r.url.toLowerCase())) continue; + settings.addServerWithCredentials(type, serverUrl: r.url); + } + } + } + + ServerType? _parseServerType(String name) { + for (final t in ServerType.values) { + if (t.name == name) return t; + } + return null; + } + + // -- libraries --------------------------------------------------------- + + Future _pushLocalLibraries(DoudouServerClient client) async { + final settings = Get.find(); + final libSync = Get.isRegistered() + ? Get.find() + : null; + if (libSync == null) return; + + for (final server in settings.servers.toList()) { + final remoteId = remoteIdFor(server.type, server.serverUrl); + syncingServerIds.add(remoteId); + var anyOk = false; + for (final kind in LibraryKind.values) { + try { + final payload = await _readLocalCache(server.id, kind); + if (payload == null) continue; + final version = await _readRevision(remoteId, kind); + await client.pushSnapshot( + musicServerId: remoteId, + kind: kind.name, + version: version, + payload: payload, + ); + anyOk = true; + } catch (_) {} + } + if (anyOk) { + syncedServerIds.add(remoteId); + } + syncingServerIds.remove(remoteId); + } + await _persistSyncedServerIds(); + } + + Future _pullRemoteLibraries(DoudouServerClient client) async { + final settings = Get.find(); + final libSync = Get.isRegistered() + ? Get.find() + : null; + if (libSync == null) return; + + for (final server in settings.servers.toList()) { + final remoteId = remoteIdFor(server.type, server.serverUrl); + syncingServerIds.add(remoteId); + try { + final remoteSnapshots = await client.listSnapshots(remoteId); + for (final snap in remoteSnapshots) { + final kind = _matchKind(snap.kind); + if (kind == null) continue; + final localVersion = await _readRevision(remoteId, kind); + if (snap.version <= localVersion) continue; + await _writeLocalCache(server.id, kind, snap.payload); + await _writeRevision(remoteId, kind, snap.version); + libSync.lastSuccessMsByKind[kind] = snap.updatedAtMs; + } + syncedServerIds.add(remoteId); + } catch (_) { + // One server failing shouldn't abort the rest. + } + syncingServerIds.remove(remoteId); + } + await _persistSyncedServerIds(); + } + + LibraryKind? _matchKind(String name) { + for (final k in LibraryKind.values) { + if (k.name == name) return k; + } + return null; + } + + Future _readLocalCache(int serverId, LibraryKind kind) async { + final boxName = switch (kind) { + LibraryKind.songs => librarySongsCacheBoxName(serverId), + LibraryKind.playlists => libraryPlaylistsCacheBoxName(serverId), + LibraryKind.albums => libraryAlbumsCacheBoxName(serverId), + LibraryKind.artists => libraryArtistsCacheBoxName(serverId), + }; + final box = Hive.isBoxOpen(boxName) ? Hive.box(boxName) : await Hive.openBox(boxName); + if (box.isEmpty) return null; + final items = box.values + .map((v) => v is Map ? Map.from(v) : v) + .toList(); + return encodeSnapshotPayload(items.cast>()); + } + + Future _writeLocalCache( + int serverId, LibraryKind kind, String payload) async { + final boxName = switch (kind) { + LibraryKind.songs => librarySongsCacheBoxName(serverId), + LibraryKind.playlists => libraryPlaylistsCacheBoxName(serverId), + LibraryKind.albums => libraryAlbumsCacheBoxName(serverId), + LibraryKind.artists => libraryArtistsCacheBoxName(serverId), + }; + final box = Hive.isBoxOpen(boxName) ? Hive.box(boxName) : await Hive.openBox(boxName); + await box.clear(); + final decoded = decodeSnapshotPayload(payload); + final map = {}; + for (var i = 0; i < decoded.length; i++) { + map[i] = decoded[i]; + } + await box.putAll(map); + } + + // -- per-server revision tracking -------------------------------------- + + String _revisionKey(String remoteId, LibraryKind kind) => + 'doudou_rev_${remoteId}_${kind.name}'; + + Future _readRevision(String remoteId, LibraryKind kind) async { + final box = Hive.isBoxOpen('AppPrefs') ? Hive.box('AppPrefs') : await Hive.openBox('AppPrefs'); + final v = box.get(_revisionKey(remoteId, kind)); + return v is int ? v : 0; + } + + Future _writeRevision(String remoteId, LibraryKind kind, int version) async { + final box = Hive.isBoxOpen('AppPrefs') ? Hive.box('AppPrefs') : await Hive.openBox('AppPrefs'); + await box.put(_revisionKey(remoteId, kind), version); + } +} + +/// Stable, content-derived ID for a music server. Identical type:url inputs +/// produce the same id across devices and builds (unlike Dart's hashCode, +/// which is not guaranteed to be stable across isolates or builds). YouTube +/// Music has no URL, so it's keyed by type alone. +String remoteIdFor(ServerType type, String? url) { + final raw = url == null || url.isEmpty + ? type.name + : '${type.name}:$url'; + return sha1.convert(utf8.encode(raw)).toString().substring(0, 16); +} diff --git a/lib/ui/screens/Settings/settings_screen.dart b/lib/ui/screens/Settings/settings_screen.dart index 4f4bd6208..80b361bad 100644 --- a/lib/ui/screens/Settings/settings_screen.dart +++ b/lib/ui/screens/Settings/settings_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import '/utils/app_l10n.dart'; import 'package:get/get.dart'; @@ -24,6 +25,8 @@ import '/ui/player/player_controller.dart'; import '/ui/utils/theme_controller.dart'; import '/ui/constants/layout.dart'; import '/models/server.dart'; +import '/models/doudou_server.dart'; +import '/services/doudou_server_sync_service.dart'; import 'settings_screen_controller.dart'; import '/app/theme/app_theme_provider.dart'; @@ -1017,12 +1020,27 @@ class _IOSSettingsViewState extends State<_IOSSettingsView> { }, child: Column( children: servers - .map((server) => ListTile( + .map((server) { + final hasUrl = + server.serverUrl?.isNotEmpty == true; + final remoteId = hasUrl + ? remoteIdFor(server.type, server.serverUrl!) + : null; + return ListTile( leading: Icon(_serverIcon(server.type)), - title: Text( - server.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, + title: Row( + children: [ + Flexible( + child: Text( + server.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (remoteId != null) + _buildServerSyncBadge( + context, remoteId, theme), + ], ), subtitle: Text( server.serverUrl?.isNotEmpty == true @@ -1094,7 +1112,8 @@ class _IOSSettingsViewState extends State<_IOSSettingsView> { ] ], ), - )) + ); + }) .toList(), ), ), @@ -1120,25 +1139,192 @@ class _IOSSettingsViewState extends State<_IOSSettingsView> { ], ); }), + const SizedBox(height: 16), + _buildDoudouServerSection(context, theme, colorScheme), ]; } + Widget _buildServerSyncBadge( + BuildContext context, + String remoteId, + ThemeData theme, + ) { + final syncService = Get.find(); + if (!syncService.isConfigured.value) return const SizedBox.shrink(); + return Obx(() { + final isSyncing = syncService.syncingServerIds.contains(remoteId); + final hasSynced = syncService.syncedServerIds.contains(remoteId); + if (!isSyncing && !hasSynced) return const SizedBox.shrink(); + + final color = isSyncing + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withValues(alpha: 0.5); + final icon = isSyncing + ? Icons.cloud_sync + : Icons.cloud_done_outlined; + final label = isSyncing ? 'syncing' : 'synced'; + + return Padding( + padding: const EdgeInsets.only(left: 6), + child: Tooltip( + message: isSyncing + ? 'Syncing with doudou-server' + : 'Synced with doudou-server', + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: color), + const SizedBox(width: 3), + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ); + }); + } + + Widget _buildDoudouServerSection( + BuildContext context, + ThemeData theme, + ColorScheme colorScheme, + ) { + final syncService = Get.find(); + return Obx(() { + final configured = syncService.isConfigured.value; + final config = syncService.config.value; + final syncing = syncService.isSyncing.value; + final lastErr = syncService.lastError.value; + final lastMs = syncService.lastSyncTimeMs.value; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 4), + child: Text( + 'doudou-server', + style: theme.textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.w700), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => _showDoudouServerDialog(context, config), + child: Ink( + decoration: BoxDecoration( + color: theme.cardColor.withValues(alpha: 0.82), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.dividerColor.withValues(alpha: 0.28), + ), + ), + child: ListTile( + dense: true, + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 2), + leading: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: colorScheme.onSurface.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + configured ? Icons.cloud_done_outlined : Icons.cloud_outlined, + size: 16, + color: colorScheme.onSurface, + ), + ), + title: Text( + configured + ? (config?.url ?? 'doudou-server') + : 'Add doudou-server', + style: theme.textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.w700), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + configured + ? (syncing + ? 'Syncing...' + : (lastErr.isNotEmpty + ? lastErr + : (lastMs != null + ? 'Last sync: ${DateTime.fromMillisecondsSinceEpoch(lastMs).toLocal()}' + : 'Connected'))) + : 'Sync your library across all your devices', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (configured) + IconButton( + icon: const Icon(Icons.sync, size: 18), + tooltip: 'Sync now', + onPressed: syncing + ? null + : () => syncService.syncNow(), + ), + Icon( + Icons.chevron_right_rounded, + color: colorScheme.onSurface.withValues(alpha: 0.6), + ), + ], + ), + ), + ), + ), + ), + ], + ); + }); + } + + Future _showDoudouServerDialog( + BuildContext context, + DoudouServerConfig? existing, + ) async { + final result = await showDialog<_DoudouServerDialogResult>( + context: context, + builder: (ctx) => _DoudouServerDialog(existing: existing), + ); + if (result == null) return; + final syncService = Get.find(); + if (result.remove) { + await syncService.setConfig(null); + } else if (result.config != null) { + await syncService.setConfig(result.config); + } + } + Future _showAddProviderPicker(BuildContext context) async { final selected = await showDialog( context: context, builder: (_) => const _AddProviderDialog(), ); if (selected == null || !context.mounted) return; - - final controller = Get.find(); - if (selected == ServerType.youtubeMusic) { - controller.addServerWithCredentials(ServerType.youtubeMusic); - } else { - showDialog( - context: context, - builder: (_) => AddServerDialog(serverType: selected), - ); - } + + showDialog( + context: context, + builder: (_) => AddServerDialog(serverType: selected), + ); } List _buildDownload( @@ -2000,6 +2186,138 @@ class _AddServerDialogState extends State { widget.serverType == ServerType.jellyfin || widget.serverType == ServerType.plex; + Widget _buildSyncWithDoudouServerButton(BuildContext context) { + final syncService = Get.find(); + return Obx(() { + if (!syncService.isConfigured.value) return const SizedBox.shrink(); + final remoteId = remoteIdFor(widget.serverType, _buildServerUrl().isEmpty + ? null + : _buildServerUrl()); + final isSyncing = syncService.syncingServerIds.contains(remoteId); + return TextButton.icon( + onPressed: isSyncing + ? null + : () async { + final settings = Get.find(); + // If editing an existing server, sync it directly. + if (widget.existing != null) { + await syncService.syncServer(widget.existing!); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Syncing with doudou-server...'), + duration: Duration(seconds: 2), + ), + ); + return; + } + // For a new server, we need to add it first, then sync. + // Find the server we just added by matching type+url. + final beforeIds = settings.servers.map((s) => s.id).toSet(); + if (_needsCredentials) { + final serverUrl = _buildServerUrl(); + if (serverUrl.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.serverUrlRequired)), + ); + return; + } + settings.addServerWithCredentials( + widget.serverType, + serverUrl: serverUrl, + username: _usernameController.text, + password: _passwordController.text, + ); + } else { + settings.addServerWithCredentials(widget.serverType); + } + final newServer = settings.servers.firstWhereOrNull( + (s) => !beforeIds.contains(s.id)); + if (newServer != null) { + unawaited(syncService.syncServer(newServer)); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Server added, syncing with doudou-server...'), + duration: Duration(seconds: 2), + ), + ); + }, + icon: isSyncing + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.cloud_sync, size: 18), + label: Text(isSyncing ? 'Syncing...' : 'Sync with doudou-server'), + ); + }); + } + + /// YTM has no credentials/URL to validate, so instead of a sync button we + /// show a badge reflecting its current doudou-server sync state. + Widget _buildYtmSyncBadge(BuildContext context) { + final syncService = Get.find(); + final theme = Theme.of(context); + return Obx(() { + if (!syncService.isConfigured.value) return const SizedBox.shrink(); + final remoteId = remoteIdFor(ServerType.youtubeMusic, null); + final isSyncing = syncService.syncingServerIds.contains(remoteId); + final hasSynced = syncService.syncedServerIds.contains(remoteId); + + final color = isSyncing + ? theme.colorScheme.primary + : hasSynced + ? theme.colorScheme.onSurface.withValues(alpha: 0.5) + : theme.colorScheme.onSurface.withValues(alpha: 0.3); + final icon = isSyncing + ? Icons.cloud_sync + : hasSynced + ? Icons.cloud_done_outlined + : Icons.cloud_outlined; + final label = isSyncing + ? 'syncing' + : hasSynced + ? 'synced' + : 'will sync'; + + return Padding( + padding: const EdgeInsets.only(top: 4), + child: Tooltip( + message: isSyncing + ? 'Syncing with doudou-server' + : hasSynced + ? 'Synced with doudou-server' + : 'Will sync with doudou-server after adding', + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ); + }); + } + String _title(BuildContext context) { final l10n = context.l10n; if (widget.existing != null) return l10n.editServer; @@ -2105,11 +2423,14 @@ class _AddServerDialogState extends State { l10n.youtubeMusicNoLogin, style: Theme.of(context).textTheme.bodyMedium, ), + _buildYtmSyncBadge(context), ], const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + if (_needsCredentials) _buildSyncWithDoudouServerButton(context), + const Spacer(), TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.cancel), @@ -2143,7 +2464,7 @@ class _AddServerDialogState extends State { ); } else { controller - .addServerWithCredentials(ServerType.youtubeMusic); + .addServerWithCredentials(widget.serverType); } } Navigator.of(context).pop(); @@ -2671,3 +2992,127 @@ String _serverTypeLabel(BuildContext context, ServerType type) { ServerType.plex => l10n.plex, }; } + +class _DoudouServerDialogResult { + _DoudouServerDialogResult.save(this.config) : remove = false; + _DoudouServerDialogResult.remove() + : config = null, + remove = true; + + final DoudouServerConfig? config; + final bool remove; +} + +class _DoudouServerDialog extends StatefulWidget { + const _DoudouServerDialog({this.existing}); + + final DoudouServerConfig? existing; + + @override + State<_DoudouServerDialog> createState() => _DoudouServerDialogState(); +} + +class _DoudouServerDialogState extends State<_DoudouServerDialog> { + late final TextEditingController _urlCtrl; + late final TextEditingController _keyCtrl; + late final TextEditingController _nameCtrl; + String? _urlError; + String? _keyError; + + @override + void initState() { + super.initState(); + _urlCtrl = TextEditingController(text: widget.existing?.url ?? ''); + _keyCtrl = TextEditingController(text: widget.existing?.key ?? ''); + _nameCtrl = TextEditingController(text: widget.existing?.deviceName ?? ''); + } + + @override + void dispose() { + _urlCtrl.dispose(); + _keyCtrl.dispose(); + _nameCtrl.dispose(); + super.dispose(); + } + + void _submit() { + final url = _urlCtrl.text.trim(); + final key = _keyCtrl.text; + var hasError = false; + setState(() { + _urlError = url.isEmpty ? 'URL is required' : null; + _keyError = key.isEmpty ? 'Password is required' : null; + hasError = _urlError != null || _keyError != null; + }); + if (hasError) return; + Navigator.of(context).pop(_DoudouServerDialogResult.save( + DoudouServerConfig( + url: url, + key: key, + deviceName: _nameCtrl.text.trim().isEmpty + ? null + : _nameCtrl.text.trim(), + ), + )); + } + + @override + Widget build(BuildContext context) { + final existing = widget.existing; + return AlertDialog( + title: Text(existing == null ? 'Add doudou-server' : 'Edit doudou-server'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _urlCtrl, + decoration: InputDecoration( + labelText: 'Server URL', + hintText: 'http://192.168.1.20:7427', + errorText: _urlError, + ), + autofocus: true, + onSubmitted: (_) => FocusScope.of(context).nextFocus(), + ), + const SizedBox(height: 12), + TextField( + controller: _keyCtrl, + decoration: InputDecoration( + labelText: 'Shared password', + hintText: 'Set via `doudou-server -set login`', + errorText: _keyError, + ), + obscureText: true, + onSubmitted: (_) => _submit(), + ), + const SizedBox(height: 12), + TextField( + controller: _nameCtrl, + decoration: const InputDecoration( + labelText: 'Device name (optional)', + hintText: 'Pixel 8, Laptop, etc.', + ), + ), + ], + ), + ), + actions: [ + if (existing != null) + TextButton( + onPressed: () => + Navigator.of(context).pop(_DoudouServerDialogResult.remove()), + child: const Text('Remove'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancel), + ), + FilledButton( + onPressed: _submit, + child: Text(existing == null ? context.l10n.add : context.l10n.save), + ), + ], + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 4339154d0..8e341e89b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin"); flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7dfa009ec..f0538047d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_acrylic + flutter_secure_storage_linux gtk media_kit_libs_linux screen_retriever_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cb16611b5..414cbe34a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import audio_service import audio_session import device_info_plus import file_picker +import flutter_secure_storage_darwin import just_audio import macos_window_utils import package_info_plus @@ -26,6 +27,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/packages/doudou_server/.gitignore b/packages/doudou_server/.gitignore new file mode 100644 index 000000000..3820a95c6 --- /dev/null +++ b/packages/doudou_server/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/doudou_server/.metadata b/packages/doudou_server/.metadata new file mode 100644 index 000000000..a834d9f55 --- /dev/null +++ b/packages/doudou_server/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ee80f08bbf97172ec030b8751ceab557177a34a6" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + - platform: linux + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + - platform: macos + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + - platform: windows + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/doudou_server/README.md b/packages/doudou_server/README.md new file mode 100644 index 000000000..3012729f5 --- /dev/null +++ b/packages/doudou_server/README.md @@ -0,0 +1,54 @@ +# doudou-server + +Headless sync server for [doudou](https://gitlab.com/Openlyst/doudou). Holds +library snapshots and music server URLs so multiple doudou clients (phones, +desktops, TVs) can stay in sync from one local server without ever exposing +media server credentials. + +## What it stores + +- Music server URLs and display metadata (Subsonic / Jellyfin / Plex / YouTube Music) +- Per-music-server library snapshots (songs, playlists, albums, artists) +- Client registrations (which devices have synced) + +## What it does NOT store + +- Media server passwords. Those stay on each doudou client. +- Audio bytes. Stream URLs are resolved by the client directly against the + music server. + +## Build + +```shell +cd packages/doudou_server +flutter build linux # or macos / windows +``` + +The resulting binary is a CLI. Run it with `-h` to see options. + +## CLI + +```shell +doudou-server -start # run the server in the foreground +doudou-server -start --host 0.0.0.0 --port 7427 +doudou-server -stop # stop a running server on this machine +doudou-server -set login # set the shared password clients use +doudou-server -status # general info +doudou-server -clients # list clients that have synced +doudou-server -health # probe the running server health endpoint +``` + +## API + +All routes are under `/api/v1` and require the `X-Doudou-Key` header (the +shared password) except `/api/v1/health` which is open. + +| Method | Path | Purpose | +|--------|---------------------------|------------------------------------------| +| GET | `/api/v1/health` | Liveness probe (unauthenticated) | +| GET | `/api/v1/servers` | List music servers | +| PUT | `/api/v1/servers` | Upsert a music server | +| DELETE | `/api/v1/servers/{id}` | Delete a music server | +| GET | `/api/v1/snapshots` | List snapshots for `?musicServerId=` | +| PUT | `/api/v1/snapshots` | Push a library snapshot | +| POST | `/api/v1/clients/register`| Register / touch a client | diff --git a/packages/doudou_server/analysis_options.yaml b/packages/doudou_server/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/packages/doudou_server/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/doudou_server/lib/main.dart b/packages/doudou_server/lib/main.dart new file mode 100644 index 000000000..7fe83d8b5 --- /dev/null +++ b/packages/doudou_server/lib/main.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:flutter/widgets.dart'; + +import 'src/cli/args.dart'; +import 'src/cli/commands.dart'; + +/// Entry point for the doudou-server CLI binary. +/// +/// This is a headless Flutter desktop app: the Flutter binding is initialised +/// (so plugins like sqlite3_flutter_libs and path_provider work on every +/// desktop platform) but no widget tree is ever mounted. The CLI parser +/// dispatches to the requested command and the process exits when done. +Future main(List argv) async { + WidgetsFlutterBinding.ensureInitialized(); + final cmd = CliCommand.parse(argv); + await runCommand(cmd); + // The desktop runners keep a message loop alive after main returns, so + // force-exit once the command is done. The daemon path returns here only + // after it has been stopped. + exit(0); +} diff --git a/packages/doudou_server/lib/src/api/protocol.dart b/packages/doudou_server/lib/src/api/protocol.dart new file mode 100644 index 000000000..a53bf3ea6 --- /dev/null +++ b/packages/doudou_server/lib/src/api/protocol.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; + +/// One entry in the music server list mirrored between doudou and doudou-server. +/// Only url + display metadata. No credentials ever travel through this. +class MusicServerDto { + MusicServerDto({ + required this.id, + required this.name, + required this.type, + required this.url, + this.isDefault = false, + }); + + final String id; + final String name; + final String type; + final String url; + final bool isDefault; + + Map toJson() => { + 'id': id, + 'name': name, + 'type': type, + 'url': url, + 'isDefault': isDefault, + }; + + factory MusicServerDto.fromJson(Map json) => MusicServerDto( + id: json['id'] as String, + name: json['name'] as String, + type: json['type'] as String, + url: json['url'] as String, + isDefault: json['isDefault'] as bool? ?? false, + ); +} + +/// Kinds of libraries we sync. Mirrors doudou's LibraryKind enum so the +/// client can map back and forth without extra translation. +const libraryKinds = ['songs', 'playlists', 'albums', 'artists']; + +class SnapshotDto { + SnapshotDto({ + required this.musicServerId, + required this.kind, + required this.version, + required this.updatedAtMs, + required this.payload, + }); + + final String musicServerId; + final String kind; + final int version; + final int updatedAtMs; + final String payload; + + Map toJson() => { + 'musicServerId': musicServerId, + 'kind': kind, + 'version': version, + 'updatedAtMs': updatedAtMs, + 'payload': payload, + }; + + factory SnapshotDto.fromJson(Map json) => SnapshotDto( + musicServerId: json['musicServerId'] as String, + kind: json['kind'] as String, + version: json['version'] as int? ?? 0, + updatedAtMs: json['updatedAtMs'] as int? ?? 0, + payload: json['payload'] as String? ?? '[]', + ); +} + +class ClientDto { + ClientDto({ + required this.id, + required this.name, + required this.firstSeenMs, + required this.lastSeenMs, + }); + + final String id; + final String name; + final int firstSeenMs; + final int lastSeenMs; + + Map toJson() => { + 'id': id, + 'name': name, + 'firstSeenMs': firstSeenMs, + 'lastSeenMs': lastSeenMs, + }; +} + +String encodeJson(Object? value) => jsonEncode(value); + +Map decodeJson(String body) => + jsonDecode(body) as Map; diff --git a/packages/doudou_server/lib/src/cli/args.dart b/packages/doudou_server/lib/src/cli/args.dart new file mode 100644 index 000000000..a6a932b65 --- /dev/null +++ b/packages/doudou_server/lib/src/cli/args.dart @@ -0,0 +1,139 @@ +import 'dart:io'; + +import 'package:args/args.dart'; + +/// Result of parsing the doudou-server CLI. Exactly one command flag is set. +class CliCommand { + CliCommand._({ + required this.kind, + this.host, + this.port, + this.username, + this.password, + }); + + final CliKind kind; + final String? host; + final int? port; + final String? username; + final String? password; + + static CliCommand parse(List argv) { + // `-set login ` doesn't fit the args package's option model, + // so pull it out and handle it before the parser sees it. + if (argv.isNotEmpty && (argv.first == '-set' || argv.first == '--set')) { + return _parseSetLogin(argv.skip(1).toList()); + } + + // The args package expects `--` for long flags, but the doudou-server CLI + // uses single-dash long flags (e.g. `-start`). Normalise so the parser + // sees `--start` etc. Short single-letter flags like `-h` are left alone. + final normalised = argv.map((a) { + if (a.startsWith('-') && !a.startsWith('--') && a.length > 2) { + return '-$a'; + } + return a; + }).toList(); + + final parser = _buildParser(); + + ArgResults results; + try { + results = parser.parse(normalised); + } on ArgParserException catch (e) { + stderr.writeln(e.message); + stderr.writeln(parser.usage); + exitWithUsage(parser); + return CliCommand._(kind: CliKind.help); // unreachable + } + + if (results['help'] as bool) { + stdout.writeln(_usage(parser)); + return CliCommand._(kind: CliKind.help); + } + + final host = results['host'] as String?; + final portStr = results['port'] as String?; + int? port; + if (portStr != null) { + port = int.tryParse(portStr); + if (port == null) { + stderr.writeln('invalid port: $portStr'); + exitWithUsage(parser); + } + } + + if (results['start'] as bool) { + return CliCommand._(kind: CliKind.start, host: host, port: port); + } + if (results['stop'] as bool) { + return CliCommand._(kind: CliKind.stop); + } + if (results['clients'] as bool) { + return CliCommand._(kind: CliKind.clients); + } + if (results['status'] as bool) { + return CliCommand._(kind: CliKind.status); + } + if (results['health'] as bool) { + return CliCommand._(kind: CliKind.health); + } + + stdout.writeln(_usage(parser)); + return CliCommand._(kind: CliKind.help); + } + + static CliCommand _parseSetLogin(List rest) { + final parser = _buildParser(); + if (rest.isEmpty || rest.first != 'login') { + stderr.writeln('only `set login ` is supported'); + exitWithUsage(parser); + } + final args = rest.skip(1).toList(); + if (args.length < 2) { + stderr.writeln('usage: doudou-server -set login '); + exitWithUsage(parser); + } + return CliCommand._( + kind: CliKind.setLogin, + username: args[0], + password: args[1], + ); + } + + static ArgParser _buildParser() => ArgParser() + ..addFlag('start', negatable: false, help: 'Start the server in the foreground.') + ..addFlag('stop', negatable: false, help: 'Stop a running server on this machine.') + ..addFlag('clients', negatable: false, help: 'List clients that have synced with this server.') + ..addFlag('status', negatable: false, help: 'Show general info about this server.') + ..addFlag('health', negatable: false, help: 'Probe the running server health endpoint.') + ..addOption('host', help: 'Host to bind to (default 0.0.0.0).') + ..addOption('port', help: 'Port to bind to (default 7427).') + ..addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.'); + + static void exitWithUsage(ArgParser parser) { + stdout.writeln(_usage(parser)); + exit(1); + } + + static String _usage(ArgParser parser) { + return [ + 'doudou-server - headless sync server for doudou', + '', + 'Usage: doudou-server [options]', + '', + 'Commands:', + ' -start Start the server (foreground)', + ' -stop Stop a running server on this machine', + ' -clients Show clients that have synced', + ' -status Show general info', + ' -health Probe the running server health endpoint', + ' -set login

Set the shared password clients must use', + '', + 'Options:', + parser.usage, + ].join('\n'); + } +} + +enum CliKind { start, stop, clients, status, health, setLogin, help } diff --git a/packages/doudou_server/lib/src/cli/commands.dart b/packages/doudou_server/lib/src/cli/commands.dart new file mode 100644 index 000000000..ac3fabc33 --- /dev/null +++ b/packages/doudou_server/lib/src/cli/commands.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import '../daemon/daemon.dart'; +import '../server/auth.dart'; +import '../storage/database.dart'; +import 'args.dart'; + +/// Dispatches a parsed [CliCommand] to its implementation. +Future runCommand(CliCommand cmd) async { + switch (cmd.kind) { + case CliKind.start: + await _start(cmd); + break; + case CliKind.stop: + await _stop(); + break; + case CliKind.clients: + await _clients(); + break; + case CliKind.status: + await _status(); + break; + case CliKind.health: + await _health(); + break; + case CliKind.setLogin: + await _setLogin(cmd); + break; + case CliKind.help: + // Help is already printed by the parser. + break; + } +} + +Future _start(CliCommand cmd) async { + final state = await readServerState(); + if (state != null && state.port != 0) { + // Best-effort liveness check: if the health endpoint answers, refuse to + // start a second instance. + if (await _probe(state.host, state.port)) { + stderr.writeln( + 'doudou-server already running on ${state.host}:${state.port} (pid ${state.pid})', + ); + exit(1); + } + } + final daemon = DoudouDaemon( + host: cmd.host ?? '0.0.0.0', + port: cmd.port ?? 7427, + ); + await daemon.run(); +} + +Future _stop() async { + final ok = await signalStop(); + if (!ok) { + stderr.writeln('no running doudou-server found'); + exit(1); + } + stdout.writeln('stop signal sent'); +} + +Future _clients() async { + final db = DoudouServerDatabase(); + try { + final rows = await db.listClients(); + if (rows.isEmpty) { + stdout.writeln('no clients have synced yet'); + return; + } + stdout.writeln('${rows.length} client(s):'); + for (final r in rows) { + stdout.writeln( + ' ${r.id} ${r.name} firstSeen=${r.firstSeen.toIso8601String()} lastSeen=${r.lastSeen.toIso8601String()}', + ); + } + } finally { + await db.close(); + } +} + +Future _status() async { + final state = await readServerState(); + final db = DoudouServerDatabase(); + try { + final servers = await db.listMusicServers(); + final clients = await db.listClients(); + final pwHash = await db.getSetting('auth.passwordHash'); + + stdout.writeln('doudou-server status'); + if (state == null) { + stdout.writeln(' running: no'); + } else { + stdout.writeln(' running: yes'); + stdout.writeln(' pid: ${state.pid}'); + stdout.writeln(' listen: ${state.host}:${state.port}'); + stdout.writeln(' startedAt: ${state.startedAt}'); + } + stdout.writeln(' password set: ${pwHash != null}'); + stdout.writeln(' music servers: ${servers.length}'); + for (final s in servers) { + stdout.writeln(' - ${s.name} [${s.type}] ${s.url}'); + } + stdout.writeln(' clients: ${clients.length}'); + } finally { + await db.close(); + } +} + +Future _health() async { + final state = await readServerState(); + if (state == null) { + stderr.writeln('no running doudou-server found'); + exit(1); + } + final ok = await _probe(state.host, state.port); + if (ok) { + stdout.writeln('healthy: ${state.host}:${state.port}'); + } else { + stderr.writeln('unhealthy: ${state.host}:${state.port} not responding'); + exit(1); + } +} + +Future _setLogin(CliCommand cmd) async { + final db = DoudouServerDatabase(); + try { + final auth = AuthMiddleware(db); + await auth.setPassword(cmd.password ?? ''); + if (cmd.username != null && cmd.username!.isNotEmpty) { + await db.putSetting('auth.username', cmd.username!); + } + stdout.writeln('login updated'); + } finally { + await db.close(); + } +} + +Future _probe(String host, int port) async { + try { + final client = HttpClient(); + final req = await client.getUrl(Uri.parse('http://$host:$port/api/v1/health')); + final res = await req.close(); + final body = await res.transform(utf8.decoder).join(); + client.close(force: true); + return res.statusCode == 200 && body.contains('"status":"ok"'); + } catch (_) { + return false; + } +} diff --git a/packages/doudou_server/lib/src/daemon/daemon.dart b/packages/doudou_server/lib/src/daemon/daemon.dart new file mode 100644 index 000000000..8b1715c05 --- /dev/null +++ b/packages/doudou_server/lib/src/daemon/daemon.dart @@ -0,0 +1,153 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import '../server/auth.dart'; +import '../server/http_server.dart'; +import '../storage/database.dart'; + +/// Runs the server in the foreground. While running, a small JSON state file +/// (the "pid file") is written to the support dir so other CLI invocations can +/// find the bound port and talk to the control endpoint. The control endpoint +/// lives on the same HTTP server as the sync API, under /api/v1/control/* and +/// protected by the shared password (except /health which is open). +class DoudouDaemon { + DoudouDaemon({required this.host, required this.port}); + + final String host; + final int port; + + late final DoudouServerDatabase db; + late final AuthMiddleware auth; + late final DoudouHttpServer http; + + Future run() async { + db = DoudouServerDatabase(); + auth = AuthMiddleware(db); + http = DoudouHttpServer(db: db, auth: auth, host: host, port: port); + + final boundPort = await http.start(); + final stateFile = await _stateFilePath(); + await _writeState(stateFile, boundPort); + + // Re-write the state file periodically so the bound port stays current + // and the file mtime reflects liveness. + final keepalive = Timer.periodic( + const Duration(seconds: 30), + (_) => _writeState(stateFile, boundPort), + ); + + final stopCompleter = Completer(); + + // Watch for a stop signal file written by `doudou-server -stop`. + final stopWatcher = _watchStopSignal(stateFile, stopCompleter); + + // Stop on SIGINT everywhere. SIGTERM only exists on non-Windows. + final sigint = ProcessSignal.sigint.watch().listen((_) { + if (!stopCompleter.isCompleted) stopCompleter.complete(); + }); + StreamSubscription? sigterm; + if (!Platform.isWindows) { + sigterm = ProcessSignal.sigterm.watch().listen((_) { + if (!stopCompleter.isCompleted) stopCompleter.complete(); + }); + } + + stdout.writeln('doudou-server listening on $host:$boundPort'); + stdout.writeln('state file: ${stateFile.path}'); + stdout.writeln('press Ctrl+C or run `doudou-server -stop` to stop'); + + await stopCompleter.future; + + keepalive.cancel(); + await stopWatcher.cancel(); + await sigint.cancel(); + await sigterm?.cancel(); + await http.stop(); + await db.close(); + try { + if (stateFile.existsSync()) stateFile.deleteSync(); + } catch (_) {} + stdout.writeln('doudou-server stopped'); + } + + Future _stateFilePath() async { + final dir = await getApplicationSupportDirectory(); + final stateDir = Directory(p.join(dir.path, 'doudou_server')); + if (!stateDir.existsSync()) stateDir.createSync(recursive: true); + return File(p.join(stateDir.path, 'server.json')); + } + + Future _writeState(File file, int boundPort) async { + final state = { + 'pid': pid, + 'host': host, + 'port': boundPort, + 'startedAt': DateTime.now().toUtc().toIso8601String(), + }; + await file.writeAsString(jsonEncode(state)); + } + + StreamSubscription _watchStopSignal( + File stateFile, + Completer stopCompleter, + ) { + final dir = stateFile.parent; + final stopFile = File(p.join(dir.path, 'stop.flag')); + final watcher = dir.watch().where((event) { + return event.type == FileSystemEvent.create && + event.path == stopFile.path; + }).listen((_) { + stopFile.deleteSync(); + if (!stopCompleter.isCompleted) stopCompleter.complete(); + }); + return watcher; + } +} + +/// Reads the state file written by a running daemon. Returns null if no daemon +/// appears to be running. +Future readServerState() async { + final dir = await getApplicationSupportDirectory(); + final file = File(p.join(dir.path, 'doudou_server', 'server.json')); + if (!file.existsSync()) return null; + try { + final json = jsonDecode(file.readAsStringSync()) as Map; + return ServerState( + pid: json['pid'] as int? ?? 0, + host: json['host'] as String? ?? '127.0.0.1', + port: json['port'] as int? ?? 0, + startedAt: json['startedAt'] as String? ?? '', + ); + } catch (_) { + return null; + } +} + +/// Writes a stop.flag file in the daemon's state dir so its watcher picks it +/// up and shuts down cleanly. +Future signalStop() async { + final state = await readServerState(); + if (state == null) return false; + final dir = await getApplicationSupportDirectory(); + final stopFile = File(p.join(dir.path, 'doudou_server', 'stop.flag')); + stopFile.writeAsStringSync(DateTime.now().toUtc().toIso8601String()); + return true; +} + +class ServerState { + ServerState({ + required this.pid, + required this.host, + required this.port, + required this.startedAt, + }); + + final int pid; + final String host; + final int port; + final String startedAt; +} diff --git a/packages/doudou_server/lib/src/server/auth.dart b/packages/doudou_server/lib/src/server/auth.dart new file mode 100644 index 000000000..71bd67155 --- /dev/null +++ b/packages/doudou_server/lib/src/server/auth.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; + +import '../storage/database.dart'; + +/// Validates the shared password sent by clients. The password is stored as a +/// sha256 hex digest in the settings table under `auth.passwordHash`. Until a +/// password is set via `doudou-server -set login`, all sync requests are +/// rejected with 401. +class AuthMiddleware { + AuthMiddleware(this._db); + + final DoudouServerDatabase _db; + + Future isConfigured() async => + (await _db.getSetting('auth.passwordHash')) != null; + + Future setPassword(String password) async { + final hash = sha256.convert(utf8.encode(password)).toString(); + await _db.putSetting('auth.passwordHash', hash); + } + + /// Returns true if the request carries a valid shared password. The password + /// may be sent in the `X-Doudou-Key` header or as a `?key=` query param. + Future handle(HttpRequest req) async { + final configured = await isConfigured(); + if (!configured) { + _unauthorized(req, 'server password not set'); + return false; + } + final provided = req.headers.value('x-doudou-key') ?? + req.uri.queryParameters['key']; + if (provided == null || provided.isEmpty) { + _unauthorized(req, 'missing key'); + return false; + } + final expected = await _db.getSetting('auth.passwordHash'); + final hash = sha256.convert(utf8.encode(provided)).toString(); + if (expected == null || hash != expected) { + _unauthorized(req, 'bad key'); + return false; + } + return true; + } + + void _unauthorized(HttpRequest req, String reason) { + req.response + ..statusCode = HttpStatus.unauthorized + ..headers.contentType = ContentType.json + ..write(jsonEncode({'error': reason})); + } +} diff --git a/packages/doudou_server/lib/src/server/http_server.dart b/packages/doudou_server/lib/src/server/http_server.dart new file mode 100644 index 000000000..455131509 --- /dev/null +++ b/packages/doudou_server/lib/src/server/http_server.dart @@ -0,0 +1,199 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/drift.dart' show Value; + +import '../api/protocol.dart'; +import '../storage/database.dart'; +import 'auth.dart'; + +/// Runs the REST API. Routes are grouped under /api/v1. The health endpoint +/// is unauthenticated so the CLI's `-health` command can probe it without a +/// password. +class DoudouHttpServer { + DoudouHttpServer({ + required this.db, + required this.auth, + required this.host, + required this.port, + }); + + final DoudouServerDatabase db; + final AuthMiddleware auth; + final String host; + final int port; + + HttpServer? _server; + + Future start() async { + _server = await HttpServer.bind(host, port); + _server!.autoCompress = true; + _server!.listen(_handleRequest); + return _server!.port; + } + + Future stop() async { + await _server?.close(force: true); + _server = null; + } + + int get boundPort => _server?.port ?? port; + + void _handleRequest(HttpRequest req) { + // Handle each request independently so long-running syncs don't block + // other clients. + _dispatch(req).catchError((e, st) { + _error(req, HttpStatus.internalServerError, '$e\n$st'); + }).whenComplete(() => req.response.close()); + } + + Future _dispatch(HttpRequest req) async { + final path = req.uri.path; + final method = req.method; + + // Health is unauthenticated. + if (path == '/api/v1/health' && method == 'GET') { + _json(req, {'status': 'ok', 'version': 1}); + return; + } + + // Everything else requires auth. + if (!await auth.handle(req)) return; + + if (path == '/api/v1/servers' && method == 'GET') { + await _listServers(req); + } else if (path == '/api/v1/servers' && method == 'PUT') { + await _upsertServer(req); + } else if (path.startsWith('/api/v1/servers/') && method == 'DELETE') { + await _deleteServer(req, path.substring('/api/v1/servers/'.length)); + } else if (path == '/api/v1/snapshots' && method == 'GET') { + await _listSnapshots(req); + } else if (path == '/api/v1/snapshots' && method == 'PUT') { + await _pushSnapshot(req); + } else if (path == '/api/v1/snapshots' && method == 'GET') { + await _listSnapshots(req); + } else if (path == '/api/v1/clients/register' && method == 'POST') { + await _registerClient(req); + } else { + _error(req, HttpStatus.notFound, 'not found: $method $path'); + } + } + + // -- routes ------------------------------------------------------------- + + Future _listServers(HttpRequest req) async { + final rows = await db.listMusicServers(); + _json(req, rows + .map((r) => MusicServerDto( + id: r.id, + name: r.name, + type: r.type, + url: r.url, + isDefault: r.isDefault, + ).toJson()) + .toList()); + } + + Future _upsertServer(HttpRequest req) async { + final body = await _readJson(req); + final dto = MusicServerDto.fromJson(body); + await db.upsertMusicServer(MusicServersCompanion.insert( + id: dto.id, + name: dto.name, + type: dto.type, + url: dto.url, + isDefault: Value(dto.isDefault), + updatedAt: DateTime.now().toUtc(), + )); + _json(req, dto.toJson()); + } + + Future _deleteServer(HttpRequest req, String id) async { + await db.deleteMusicServer(id); + _json(req, {'deleted': id}); + } + + Future _listSnapshots(HttpRequest req) async { + final serverId = req.uri.queryParameters['musicServerId']; + if (serverId == null) { + _error(req, HttpStatus.badRequest, 'missing musicServerId'); + return; + } + final rows = await db.listSnapshots(serverId); + _json(req, rows + .map((r) => SnapshotDto( + musicServerId: r.musicServerId, + kind: r.kind, + version: r.version, + updatedAtMs: r.updatedAt.millisecondsSinceEpoch, + payload: r.payload, + ).toJson()) + .toList()); + } + + Future _pushSnapshot(HttpRequest req) async { + final body = await _readJson(req); + final dto = SnapshotDto.fromJson(body); + final existing = await db.getSnapshot(dto.musicServerId, dto.kind); + // Server is source of truth: accept the push if the client's version is + // newer than what we have, or if we have nothing yet. + if (existing != null && dto.version <= existing.version) { + _json(req, { + 'accepted': false, + 'version': existing.version, + 'reason': 'stale', + }); + return; + } + final nextVersion = dto.version == 0 + ? (existing?.version ?? 0) + 1 + : dto.version; + await db.saveSnapshot( + musicServerId: dto.musicServerId, + kind: dto.kind, + payload: dto.payload, + version: nextVersion, + ); + _json(req, {'accepted': true, 'version': nextVersion}); + } + + Future _registerClient(HttpRequest req) async { + final body = await _readJson(req); + final id = body['id'] as String? ?? ''; + final name = body['name'] as String? ?? 'client'; + if (id.isEmpty) { + _error(req, HttpStatus.badRequest, 'missing id'); + return; + } + await db.touchClient(id, name); + _json(req, {'registered': id}); + } + + // -- helpers ------------------------------------------------------------ + + Future> _readJson(HttpRequest req) async { + final bytes = []; + await for (final chunk in req) { + bytes.addAll(chunk); + } + if (bytes.isEmpty) return {}; + final raw = utf8.decode(bytes, allowMalformed: true); + if (raw.isEmpty) return {}; + return jsonDecode(raw) as Map; + } + + void _json(HttpRequest req, Object? body) { + req.response + ..statusCode = HttpStatus.ok + ..headers.contentType = ContentType.json + ..write(jsonEncode(body)); + } + + void _error(HttpRequest req, int status, String message) { + req.response + ..statusCode = status + ..headers.contentType = ContentType.json + ..write(jsonEncode({'error': message})); + } +} diff --git a/packages/doudou_server/lib/src/storage/database.dart b/packages/doudou_server/lib/src/storage/database.dart new file mode 100644 index 000000000..3d4a56ca5 --- /dev/null +++ b/packages/doudou_server/lib/src/storage/database.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import 'tables.dart'; + +part 'database.g.dart'; + +@DriftDatabase(tables: [MusicServers, LibrarySnapshots, Clients, Settings]) +class DoudouServerDatabase extends _$DoudouServerDatabase { + DoudouServerDatabase() : super(_open()); + + DoudouServerDatabase.forTesting(super.e); + + @override + int get schemaVersion => 1; + + // -- settings ----------------------------------------------------------- + + Future getSetting(String key) async { + final row = await (select(settings)..where((t) => t.key.equals(key))) + .getSingleOrNull(); + return row?.value; + } + + Future putSetting(String key, String value) async { + await into(settings).insertOnConflictUpdate( + SettingRow(key: key, value: value), + ); + } + + // -- music servers ------------------------------------------------------ + + Future> listMusicServers() => + select(musicServers).get(); + + Future findMusicServer(String id) async { + final q = select(musicServers)..where((t) => t.id.equals(id)); + return q.getSingleOrNull(); + } + + Future upsertMusicServer(MusicServersCompanion entry) => + into(musicServers).insertOnConflictUpdate(entry); + + Future deleteMusicServer(String id) { + return transaction(() async { + await (delete(librarySnapshots) + ..where((t) => t.musicServerId.equals(id))) + .go(); + await (delete(musicServers)..where((t) => t.id.equals(id))).go(); + }); + } + + // -- library snapshots -------------------------------------------------- + + Future getSnapshot(String serverId, String kind) async { + final q = select(librarySnapshots) + ..where((t) => t.musicServerId.equals(serverId)) + ..where((t) => t.kind.equals(kind)); + return q.getSingleOrNull(); + } + + Future> listSnapshots(String serverId) async { + final q = select(librarySnapshots) + ..where((t) => t.musicServerId.equals(serverId)); + return q.get(); + } + + Future saveSnapshot({ + required String musicServerId, + required String kind, + required String payload, + required int version, + }) async { + await into(librarySnapshots).insertOnConflictUpdate( + LibrarySnapshotsCompanion.insert( + musicServerId: musicServerId, + kind: kind, + payload: payload, + version: Value(version), + updatedAt: DateTime.now().toUtc(), + ), + ); + } + + // -- clients ------------------------------------------------------------ + + Future> listClients() => select(clients).get(); + + Future touchClient(String id, String name) async { + final now = DateTime.now().toUtc(); + final existing = await (select(clients)..where((t) => t.id.equals(id))) + .getSingleOrNull(); + if (existing == null) { + await into(clients).insert(ClientsCompanion.insert( + id: id, + name: name, + firstSeen: now, + lastSeen: now, + )); + } else { + await (update(clients)..where((t) => t.id.equals(id))) + .write(ClientsCompanion(lastSeen: Value(now), name: Value(name))); + } + } +} + +LazyDatabase _open() { + return LazyDatabase(() async { + final dir = await getApplicationSupportDirectory(); + final dbDir = Directory(p.join(dir.path, 'doudou_server')); + if (!dbDir.existsSync()) dbDir.createSync(recursive: true); + final file = File(p.join(dbDir.path, 'doudou_server.sqlite')); + return NativeDatabase.createInBackground(file); + }); +} diff --git a/packages/doudou_server/lib/src/storage/database.g.dart b/packages/doudou_server/lib/src/storage/database.g.dart new file mode 100644 index 000000000..add9fe399 --- /dev/null +++ b/packages/doudou_server/lib/src/storage/database.g.dart @@ -0,0 +1,2090 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $MusicServersTable extends MusicServers + with TableInfo<$MusicServersTable, MusicServerRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MusicServersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _urlMeta = const VerificationMeta('url'); + @override + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _isDefaultMeta = const VerificationMeta( + 'isDefault', + ); + @override + late final GeneratedColumn isDefault = GeneratedColumn( + 'is_default', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_default" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + type, + url, + isDefault, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'music_servers'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('url')) { + context.handle( + _urlMeta, + url.isAcceptableOrUnknown(data['url']!, _urlMeta), + ); + } else if (isInserting) { + context.missing(_urlMeta); + } + if (data.containsKey('is_default')) { + context.handle( + _isDefaultMeta, + isDefault.isAcceptableOrUnknown(data['is_default']!, _isDefaultMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + MusicServerRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MusicServerRow( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + url: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url'], + )!, + isDefault: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_default'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $MusicServersTable createAlias(String alias) { + return $MusicServersTable(attachedDatabase, alias); + } +} + +class MusicServerRow extends DataClass implements Insertable { + final String id; + final String name; + final String type; + final String url; + final bool isDefault; + final DateTime updatedAt; + const MusicServerRow({ + required this.id, + required this.name, + required this.type, + required this.url, + required this.isDefault, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['type'] = Variable(type); + map['url'] = Variable(url); + map['is_default'] = Variable(isDefault); + map['updated_at'] = Variable(updatedAt); + return map; + } + + MusicServersCompanion toCompanion(bool nullToAbsent) { + return MusicServersCompanion( + id: Value(id), + name: Value(name), + type: Value(type), + url: Value(url), + isDefault: Value(isDefault), + updatedAt: Value(updatedAt), + ); + } + + factory MusicServerRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MusicServerRow( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + url: serializer.fromJson(json['url']), + isDefault: serializer.fromJson(json['isDefault']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'url': serializer.toJson(url), + 'isDefault': serializer.toJson(isDefault), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + MusicServerRow copyWith({ + String? id, + String? name, + String? type, + String? url, + bool? isDefault, + DateTime? updatedAt, + }) => MusicServerRow( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + url: url ?? this.url, + isDefault: isDefault ?? this.isDefault, + updatedAt: updatedAt ?? this.updatedAt, + ); + MusicServerRow copyWithCompanion(MusicServersCompanion data) { + return MusicServerRow( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + url: data.url.present ? data.url.value : this.url, + isDefault: data.isDefault.present ? data.isDefault.value : this.isDefault, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('MusicServerRow(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('url: $url, ') + ..write('isDefault: $isDefault, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, type, url, isDefault, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MusicServerRow && + other.id == this.id && + other.name == this.name && + other.type == this.type && + other.url == this.url && + other.isDefault == this.isDefault && + other.updatedAt == this.updatedAt); +} + +class MusicServersCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value type; + final Value url; + final Value isDefault; + final Value updatedAt; + final Value rowid; + const MusicServersCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.type = const Value.absent(), + this.url = const Value.absent(), + this.isDefault = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MusicServersCompanion.insert({ + required String id, + required String name, + required String type, + required String url, + this.isDefault = const Value.absent(), + required DateTime updatedAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + type = Value(type), + url = Value(url), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? type, + Expression? url, + Expression? isDefault, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (url != null) 'url': url, + if (isDefault != null) 'is_default': isDefault, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MusicServersCompanion copyWith({ + Value? id, + Value? name, + Value? type, + Value? url, + Value? isDefault, + Value? updatedAt, + Value? rowid, + }) { + return MusicServersCompanion( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + url: url ?? this.url, + isDefault: isDefault ?? this.isDefault, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (url.present) { + map['url'] = Variable(url.value); + } + if (isDefault.present) { + map['is_default'] = Variable(isDefault.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MusicServersCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('url: $url, ') + ..write('isDefault: $isDefault, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $LibrarySnapshotsTable extends LibrarySnapshots + with TableInfo<$LibrarySnapshotsTable, LibrarySnapshotRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $LibrarySnapshotsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _musicServerIdMeta = const VerificationMeta( + 'musicServerId', + ); + @override + late final GeneratedColumn musicServerId = GeneratedColumn( + 'music_server_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _kindMeta = const VerificationMeta('kind'); + @override + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _payloadMeta = const VerificationMeta( + 'payload', + ); + @override + late final GeneratedColumn payload = GeneratedColumn( + 'payload', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _versionMeta = const VerificationMeta( + 'version', + ); + @override + late final GeneratedColumn version = GeneratedColumn( + 'version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + musicServerId, + kind, + payload, + version, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'library_snapshots'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('music_server_id')) { + context.handle( + _musicServerIdMeta, + musicServerId.isAcceptableOrUnknown( + data['music_server_id']!, + _musicServerIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_musicServerIdMeta); + } + if (data.containsKey('kind')) { + context.handle( + _kindMeta, + kind.isAcceptableOrUnknown(data['kind']!, _kindMeta), + ); + } else if (isInserting) { + context.missing(_kindMeta); + } + if (data.containsKey('payload')) { + context.handle( + _payloadMeta, + payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta), + ); + } else if (isInserting) { + context.missing(_payloadMeta); + } + if (data.containsKey('version')) { + context.handle( + _versionMeta, + version.isAcceptableOrUnknown(data['version']!, _versionMeta), + ); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {musicServerId, kind}; + @override + LibrarySnapshotRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LibrarySnapshotRow( + musicServerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}music_server_id'], + )!, + kind: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}kind'], + )!, + payload: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}payload'], + )!, + version: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}version'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $LibrarySnapshotsTable createAlias(String alias) { + return $LibrarySnapshotsTable(attachedDatabase, alias); + } +} + +class LibrarySnapshotRow extends DataClass + implements Insertable { + final String musicServerId; + final String kind; + final String payload; + final int version; + final DateTime updatedAt; + const LibrarySnapshotRow({ + required this.musicServerId, + required this.kind, + required this.payload, + required this.version, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['music_server_id'] = Variable(musicServerId); + map['kind'] = Variable(kind); + map['payload'] = Variable(payload); + map['version'] = Variable(version); + map['updated_at'] = Variable(updatedAt); + return map; + } + + LibrarySnapshotsCompanion toCompanion(bool nullToAbsent) { + return LibrarySnapshotsCompanion( + musicServerId: Value(musicServerId), + kind: Value(kind), + payload: Value(payload), + version: Value(version), + updatedAt: Value(updatedAt), + ); + } + + factory LibrarySnapshotRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LibrarySnapshotRow( + musicServerId: serializer.fromJson(json['musicServerId']), + kind: serializer.fromJson(json['kind']), + payload: serializer.fromJson(json['payload']), + version: serializer.fromJson(json['version']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'musicServerId': serializer.toJson(musicServerId), + 'kind': serializer.toJson(kind), + 'payload': serializer.toJson(payload), + 'version': serializer.toJson(version), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + LibrarySnapshotRow copyWith({ + String? musicServerId, + String? kind, + String? payload, + int? version, + DateTime? updatedAt, + }) => LibrarySnapshotRow( + musicServerId: musicServerId ?? this.musicServerId, + kind: kind ?? this.kind, + payload: payload ?? this.payload, + version: version ?? this.version, + updatedAt: updatedAt ?? this.updatedAt, + ); + LibrarySnapshotRow copyWithCompanion(LibrarySnapshotsCompanion data) { + return LibrarySnapshotRow( + musicServerId: data.musicServerId.present + ? data.musicServerId.value + : this.musicServerId, + kind: data.kind.present ? data.kind.value : this.kind, + payload: data.payload.present ? data.payload.value : this.payload, + version: data.version.present ? data.version.value : this.version, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('LibrarySnapshotRow(') + ..write('musicServerId: $musicServerId, ') + ..write('kind: $kind, ') + ..write('payload: $payload, ') + ..write('version: $version, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(musicServerId, kind, payload, version, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LibrarySnapshotRow && + other.musicServerId == this.musicServerId && + other.kind == this.kind && + other.payload == this.payload && + other.version == this.version && + other.updatedAt == this.updatedAt); +} + +class LibrarySnapshotsCompanion extends UpdateCompanion { + final Value musicServerId; + final Value kind; + final Value payload; + final Value version; + final Value updatedAt; + final Value rowid; + const LibrarySnapshotsCompanion({ + this.musicServerId = const Value.absent(), + this.kind = const Value.absent(), + this.payload = const Value.absent(), + this.version = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + LibrarySnapshotsCompanion.insert({ + required String musicServerId, + required String kind, + required String payload, + this.version = const Value.absent(), + required DateTime updatedAt, + this.rowid = const Value.absent(), + }) : musicServerId = Value(musicServerId), + kind = Value(kind), + payload = Value(payload), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? musicServerId, + Expression? kind, + Expression? payload, + Expression? version, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (musicServerId != null) 'music_server_id': musicServerId, + if (kind != null) 'kind': kind, + if (payload != null) 'payload': payload, + if (version != null) 'version': version, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + LibrarySnapshotsCompanion copyWith({ + Value? musicServerId, + Value? kind, + Value? payload, + Value? version, + Value? updatedAt, + Value? rowid, + }) { + return LibrarySnapshotsCompanion( + musicServerId: musicServerId ?? this.musicServerId, + kind: kind ?? this.kind, + payload: payload ?? this.payload, + version: version ?? this.version, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (musicServerId.present) { + map['music_server_id'] = Variable(musicServerId.value); + } + if (kind.present) { + map['kind'] = Variable(kind.value); + } + if (payload.present) { + map['payload'] = Variable(payload.value); + } + if (version.present) { + map['version'] = Variable(version.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LibrarySnapshotsCompanion(') + ..write('musicServerId: $musicServerId, ') + ..write('kind: $kind, ') + ..write('payload: $payload, ') + ..write('version: $version, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ClientsTable extends Clients with TableInfo<$ClientsTable, ClientRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ClientsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _firstSeenMeta = const VerificationMeta( + 'firstSeen', + ); + @override + late final GeneratedColumn firstSeen = GeneratedColumn( + 'first_seen', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _lastSeenMeta = const VerificationMeta( + 'lastSeen', + ); + @override + late final GeneratedColumn lastSeen = GeneratedColumn( + 'last_seen', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, name, firstSeen, lastSeen]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'clients'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('first_seen')) { + context.handle( + _firstSeenMeta, + firstSeen.isAcceptableOrUnknown(data['first_seen']!, _firstSeenMeta), + ); + } else if (isInserting) { + context.missing(_firstSeenMeta); + } + if (data.containsKey('last_seen')) { + context.handle( + _lastSeenMeta, + lastSeen.isAcceptableOrUnknown(data['last_seen']!, _lastSeenMeta), + ); + } else if (isInserting) { + context.missing(_lastSeenMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ClientRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ClientRow( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + firstSeen: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}first_seen'], + )!, + lastSeen: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_seen'], + )!, + ); + } + + @override + $ClientsTable createAlias(String alias) { + return $ClientsTable(attachedDatabase, alias); + } +} + +class ClientRow extends DataClass implements Insertable { + final String id; + final String name; + final DateTime firstSeen; + final DateTime lastSeen; + const ClientRow({ + required this.id, + required this.name, + required this.firstSeen, + required this.lastSeen, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['first_seen'] = Variable(firstSeen); + map['last_seen'] = Variable(lastSeen); + return map; + } + + ClientsCompanion toCompanion(bool nullToAbsent) { + return ClientsCompanion( + id: Value(id), + name: Value(name), + firstSeen: Value(firstSeen), + lastSeen: Value(lastSeen), + ); + } + + factory ClientRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ClientRow( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + firstSeen: serializer.fromJson(json['firstSeen']), + lastSeen: serializer.fromJson(json['lastSeen']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'firstSeen': serializer.toJson(firstSeen), + 'lastSeen': serializer.toJson(lastSeen), + }; + } + + ClientRow copyWith({ + String? id, + String? name, + DateTime? firstSeen, + DateTime? lastSeen, + }) => ClientRow( + id: id ?? this.id, + name: name ?? this.name, + firstSeen: firstSeen ?? this.firstSeen, + lastSeen: lastSeen ?? this.lastSeen, + ); + ClientRow copyWithCompanion(ClientsCompanion data) { + return ClientRow( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + firstSeen: data.firstSeen.present ? data.firstSeen.value : this.firstSeen, + lastSeen: data.lastSeen.present ? data.lastSeen.value : this.lastSeen, + ); + } + + @override + String toString() { + return (StringBuffer('ClientRow(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('firstSeen: $firstSeen, ') + ..write('lastSeen: $lastSeen') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, firstSeen, lastSeen); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ClientRow && + other.id == this.id && + other.name == this.name && + other.firstSeen == this.firstSeen && + other.lastSeen == this.lastSeen); +} + +class ClientsCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value firstSeen; + final Value lastSeen; + final Value rowid; + const ClientsCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.firstSeen = const Value.absent(), + this.lastSeen = const Value.absent(), + this.rowid = const Value.absent(), + }); + ClientsCompanion.insert({ + required String id, + required String name, + required DateTime firstSeen, + required DateTime lastSeen, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + firstSeen = Value(firstSeen), + lastSeen = Value(lastSeen); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? firstSeen, + Expression? lastSeen, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (firstSeen != null) 'first_seen': firstSeen, + if (lastSeen != null) 'last_seen': lastSeen, + if (rowid != null) 'rowid': rowid, + }); + } + + ClientsCompanion copyWith({ + Value? id, + Value? name, + Value? firstSeen, + Value? lastSeen, + Value? rowid, + }) { + return ClientsCompanion( + id: id ?? this.id, + name: name ?? this.name, + firstSeen: firstSeen ?? this.firstSeen, + lastSeen: lastSeen ?? this.lastSeen, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (firstSeen.present) { + map['first_seen'] = Variable(firstSeen.value); + } + if (lastSeen.present) { + map['last_seen'] = Variable(lastSeen.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ClientsCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('firstSeen: $firstSeen, ') + ..write('lastSeen: $lastSeen, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SettingsTable extends Settings + with TableInfo<$SettingsTable, SettingRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SettingsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _keyMeta = const VerificationMeta('key'); + @override + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + @override + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'settings'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('key')) { + context.handle( + _keyMeta, + key.isAcceptableOrUnknown(data['key']!, _keyMeta), + ); + } else if (isInserting) { + context.missing(_keyMeta); + } + if (data.containsKey('value')) { + context.handle( + _valueMeta, + value.isAcceptableOrUnknown(data['value']!, _valueMeta), + ); + } else if (isInserting) { + context.missing(_valueMeta); + } + return context; + } + + @override + Set get $primaryKey => {key}; + @override + SettingRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingRow( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + $SettingsTable createAlias(String alias) { + return $SettingsTable(attachedDatabase, alias); + } +} + +class SettingRow extends DataClass implements Insertable { + final String key; + final String value; + const SettingRow({required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + SettingsCompanion toCompanion(bool nullToAbsent) { + return SettingsCompanion(key: Value(key), value: Value(value)); + } + + factory SettingRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingRow( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + SettingRow copyWith({String? key, String? value}) => + SettingRow(key: key ?? this.key, value: value ?? this.value); + SettingRow copyWithCompanion(SettingsCompanion data) { + return SettingRow( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingRow(') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingRow && + other.key == this.key && + other.value == this.value); +} + +class SettingsCompanion extends UpdateCompanion { + final Value key; + final Value value; + final Value rowid; + const SettingsCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingsCompanion.insert({ + required String key, + required String value, + this.rowid = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingsCompanion copyWith({ + Value? key, + Value? value, + Value? rowid, + }) { + return SettingsCompanion( + key: key ?? this.key, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingsCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$DoudouServerDatabase extends GeneratedDatabase { + _$DoudouServerDatabase(QueryExecutor e) : super(e); + $DoudouServerDatabaseManager get managers => + $DoudouServerDatabaseManager(this); + late final $MusicServersTable musicServers = $MusicServersTable(this); + late final $LibrarySnapshotsTable librarySnapshots = $LibrarySnapshotsTable( + this, + ); + late final $ClientsTable clients = $ClientsTable(this); + late final $SettingsTable settings = $SettingsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + musicServers, + librarySnapshots, + clients, + settings, + ]; +} + +typedef $$MusicServersTableCreateCompanionBuilder = + MusicServersCompanion Function({ + required String id, + required String name, + required String type, + required String url, + Value isDefault, + required DateTime updatedAt, + Value rowid, + }); +typedef $$MusicServersTableUpdateCompanionBuilder = + MusicServersCompanion Function({ + Value id, + Value name, + Value type, + Value url, + Value isDefault, + Value updatedAt, + Value rowid, + }); + +class $$MusicServersTableFilterComposer + extends Composer<_$DoudouServerDatabase, $MusicServersTable> { + $$MusicServersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get url => $composableBuilder( + column: $table.url, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isDefault => $composableBuilder( + column: $table.isDefault, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$MusicServersTableOrderingComposer + extends Composer<_$DoudouServerDatabase, $MusicServersTable> { + $$MusicServersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get url => $composableBuilder( + column: $table.url, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isDefault => $composableBuilder( + column: $table.isDefault, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$MusicServersTableAnnotationComposer + extends Composer<_$DoudouServerDatabase, $MusicServersTable> { + $$MusicServersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get url => + $composableBuilder(column: $table.url, builder: (column) => column); + + GeneratedColumn get isDefault => + $composableBuilder(column: $table.isDefault, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); +} + +class $$MusicServersTableTableManager + extends + RootTableManager< + _$DoudouServerDatabase, + $MusicServersTable, + MusicServerRow, + $$MusicServersTableFilterComposer, + $$MusicServersTableOrderingComposer, + $$MusicServersTableAnnotationComposer, + $$MusicServersTableCreateCompanionBuilder, + $$MusicServersTableUpdateCompanionBuilder, + ( + MusicServerRow, + BaseReferences< + _$DoudouServerDatabase, + $MusicServersTable, + MusicServerRow + >, + ), + MusicServerRow, + PrefetchHooks Function() + > { + $$MusicServersTableTableManager( + _$DoudouServerDatabase db, + $MusicServersTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MusicServersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MusicServersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MusicServersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value type = const Value.absent(), + Value url = const Value.absent(), + Value isDefault = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => MusicServersCompanion( + id: id, + name: name, + type: type, + url: url, + isDefault: isDefault, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required String type, + required String url, + Value isDefault = const Value.absent(), + required DateTime updatedAt, + Value rowid = const Value.absent(), + }) => MusicServersCompanion.insert( + id: id, + name: name, + type: type, + url: url, + isDefault: isDefault, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$MusicServersTableProcessedTableManager = + ProcessedTableManager< + _$DoudouServerDatabase, + $MusicServersTable, + MusicServerRow, + $$MusicServersTableFilterComposer, + $$MusicServersTableOrderingComposer, + $$MusicServersTableAnnotationComposer, + $$MusicServersTableCreateCompanionBuilder, + $$MusicServersTableUpdateCompanionBuilder, + ( + MusicServerRow, + BaseReferences< + _$DoudouServerDatabase, + $MusicServersTable, + MusicServerRow + >, + ), + MusicServerRow, + PrefetchHooks Function() + >; +typedef $$LibrarySnapshotsTableCreateCompanionBuilder = + LibrarySnapshotsCompanion Function({ + required String musicServerId, + required String kind, + required String payload, + Value version, + required DateTime updatedAt, + Value rowid, + }); +typedef $$LibrarySnapshotsTableUpdateCompanionBuilder = + LibrarySnapshotsCompanion Function({ + Value musicServerId, + Value kind, + Value payload, + Value version, + Value updatedAt, + Value rowid, + }); + +class $$LibrarySnapshotsTableFilterComposer + extends Composer<_$DoudouServerDatabase, $LibrarySnapshotsTable> { + $$LibrarySnapshotsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get musicServerId => $composableBuilder( + column: $table.musicServerId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get kind => $composableBuilder( + column: $table.kind, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get payload => $composableBuilder( + column: $table.payload, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get version => $composableBuilder( + column: $table.version, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$LibrarySnapshotsTableOrderingComposer + extends Composer<_$DoudouServerDatabase, $LibrarySnapshotsTable> { + $$LibrarySnapshotsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get musicServerId => $composableBuilder( + column: $table.musicServerId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get kind => $composableBuilder( + column: $table.kind, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get payload => $composableBuilder( + column: $table.payload, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get version => $composableBuilder( + column: $table.version, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$LibrarySnapshotsTableAnnotationComposer + extends Composer<_$DoudouServerDatabase, $LibrarySnapshotsTable> { + $$LibrarySnapshotsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get musicServerId => $composableBuilder( + column: $table.musicServerId, + builder: (column) => column, + ); + + GeneratedColumn get kind => + $composableBuilder(column: $table.kind, builder: (column) => column); + + GeneratedColumn get payload => + $composableBuilder(column: $table.payload, builder: (column) => column); + + GeneratedColumn get version => + $composableBuilder(column: $table.version, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); +} + +class $$LibrarySnapshotsTableTableManager + extends + RootTableManager< + _$DoudouServerDatabase, + $LibrarySnapshotsTable, + LibrarySnapshotRow, + $$LibrarySnapshotsTableFilterComposer, + $$LibrarySnapshotsTableOrderingComposer, + $$LibrarySnapshotsTableAnnotationComposer, + $$LibrarySnapshotsTableCreateCompanionBuilder, + $$LibrarySnapshotsTableUpdateCompanionBuilder, + ( + LibrarySnapshotRow, + BaseReferences< + _$DoudouServerDatabase, + $LibrarySnapshotsTable, + LibrarySnapshotRow + >, + ), + LibrarySnapshotRow, + PrefetchHooks Function() + > { + $$LibrarySnapshotsTableTableManager( + _$DoudouServerDatabase db, + $LibrarySnapshotsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$LibrarySnapshotsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$LibrarySnapshotsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$LibrarySnapshotsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value musicServerId = const Value.absent(), + Value kind = const Value.absent(), + Value payload = const Value.absent(), + Value version = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => LibrarySnapshotsCompanion( + musicServerId: musicServerId, + kind: kind, + payload: payload, + version: version, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String musicServerId, + required String kind, + required String payload, + Value version = const Value.absent(), + required DateTime updatedAt, + Value rowid = const Value.absent(), + }) => LibrarySnapshotsCompanion.insert( + musicServerId: musicServerId, + kind: kind, + payload: payload, + version: version, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$LibrarySnapshotsTableProcessedTableManager = + ProcessedTableManager< + _$DoudouServerDatabase, + $LibrarySnapshotsTable, + LibrarySnapshotRow, + $$LibrarySnapshotsTableFilterComposer, + $$LibrarySnapshotsTableOrderingComposer, + $$LibrarySnapshotsTableAnnotationComposer, + $$LibrarySnapshotsTableCreateCompanionBuilder, + $$LibrarySnapshotsTableUpdateCompanionBuilder, + ( + LibrarySnapshotRow, + BaseReferences< + _$DoudouServerDatabase, + $LibrarySnapshotsTable, + LibrarySnapshotRow + >, + ), + LibrarySnapshotRow, + PrefetchHooks Function() + >; +typedef $$ClientsTableCreateCompanionBuilder = + ClientsCompanion Function({ + required String id, + required String name, + required DateTime firstSeen, + required DateTime lastSeen, + Value rowid, + }); +typedef $$ClientsTableUpdateCompanionBuilder = + ClientsCompanion Function({ + Value id, + Value name, + Value firstSeen, + Value lastSeen, + Value rowid, + }); + +class $$ClientsTableFilterComposer + extends Composer<_$DoudouServerDatabase, $ClientsTable> { + $$ClientsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get firstSeen => $composableBuilder( + column: $table.firstSeen, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastSeen => $composableBuilder( + column: $table.lastSeen, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ClientsTableOrderingComposer + extends Composer<_$DoudouServerDatabase, $ClientsTable> { + $$ClientsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get firstSeen => $composableBuilder( + column: $table.firstSeen, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastSeen => $composableBuilder( + column: $table.lastSeen, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ClientsTableAnnotationComposer + extends Composer<_$DoudouServerDatabase, $ClientsTable> { + $$ClientsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get firstSeen => + $composableBuilder(column: $table.firstSeen, builder: (column) => column); + + GeneratedColumn get lastSeen => + $composableBuilder(column: $table.lastSeen, builder: (column) => column); +} + +class $$ClientsTableTableManager + extends + RootTableManager< + _$DoudouServerDatabase, + $ClientsTable, + ClientRow, + $$ClientsTableFilterComposer, + $$ClientsTableOrderingComposer, + $$ClientsTableAnnotationComposer, + $$ClientsTableCreateCompanionBuilder, + $$ClientsTableUpdateCompanionBuilder, + ( + ClientRow, + BaseReferences<_$DoudouServerDatabase, $ClientsTable, ClientRow>, + ), + ClientRow, + PrefetchHooks Function() + > { + $$ClientsTableTableManager(_$DoudouServerDatabase db, $ClientsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ClientsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ClientsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ClientsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value firstSeen = const Value.absent(), + Value lastSeen = const Value.absent(), + Value rowid = const Value.absent(), + }) => ClientsCompanion( + id: id, + name: name, + firstSeen: firstSeen, + lastSeen: lastSeen, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required DateTime firstSeen, + required DateTime lastSeen, + Value rowid = const Value.absent(), + }) => ClientsCompanion.insert( + id: id, + name: name, + firstSeen: firstSeen, + lastSeen: lastSeen, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ClientsTableProcessedTableManager = + ProcessedTableManager< + _$DoudouServerDatabase, + $ClientsTable, + ClientRow, + $$ClientsTableFilterComposer, + $$ClientsTableOrderingComposer, + $$ClientsTableAnnotationComposer, + $$ClientsTableCreateCompanionBuilder, + $$ClientsTableUpdateCompanionBuilder, + ( + ClientRow, + BaseReferences<_$DoudouServerDatabase, $ClientsTable, ClientRow>, + ), + ClientRow, + PrefetchHooks Function() + >; +typedef $$SettingsTableCreateCompanionBuilder = + SettingsCompanion Function({ + required String key, + required String value, + Value rowid, + }); +typedef $$SettingsTableUpdateCompanionBuilder = + SettingsCompanion Function({ + Value key, + Value value, + Value rowid, + }); + +class $$SettingsTableFilterComposer + extends Composer<_$DoudouServerDatabase, $SettingsTable> { + $$SettingsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get key => $composableBuilder( + column: $table.key, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnFilters(column), + ); +} + +class $$SettingsTableOrderingComposer + extends Composer<_$DoudouServerDatabase, $SettingsTable> { + $$SettingsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get key => $composableBuilder( + column: $table.key, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$SettingsTableAnnotationComposer + extends Composer<_$DoudouServerDatabase, $SettingsTable> { + $$SettingsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get key => + $composableBuilder(column: $table.key, builder: (column) => column); + + GeneratedColumn get value => + $composableBuilder(column: $table.value, builder: (column) => column); +} + +class $$SettingsTableTableManager + extends + RootTableManager< + _$DoudouServerDatabase, + $SettingsTable, + SettingRow, + $$SettingsTableFilterComposer, + $$SettingsTableOrderingComposer, + $$SettingsTableAnnotationComposer, + $$SettingsTableCreateCompanionBuilder, + $$SettingsTableUpdateCompanionBuilder, + ( + SettingRow, + BaseReferences<_$DoudouServerDatabase, $SettingsTable, SettingRow>, + ), + SettingRow, + PrefetchHooks Function() + > { + $$SettingsTableTableManager(_$DoudouServerDatabase db, $SettingsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SettingsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SettingsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SettingsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value key = const Value.absent(), + Value value = const Value.absent(), + Value rowid = const Value.absent(), + }) => SettingsCompanion(key: key, value: value, rowid: rowid), + createCompanionCallback: + ({ + required String key, + required String value, + Value rowid = const Value.absent(), + }) => SettingsCompanion.insert( + key: key, + value: value, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$SettingsTableProcessedTableManager = + ProcessedTableManager< + _$DoudouServerDatabase, + $SettingsTable, + SettingRow, + $$SettingsTableFilterComposer, + $$SettingsTableOrderingComposer, + $$SettingsTableAnnotationComposer, + $$SettingsTableCreateCompanionBuilder, + $$SettingsTableUpdateCompanionBuilder, + ( + SettingRow, + BaseReferences<_$DoudouServerDatabase, $SettingsTable, SettingRow>, + ), + SettingRow, + PrefetchHooks Function() + >; + +class $DoudouServerDatabaseManager { + final _$DoudouServerDatabase _db; + $DoudouServerDatabaseManager(this._db); + $$MusicServersTableTableManager get musicServers => + $$MusicServersTableTableManager(_db, _db.musicServers); + $$LibrarySnapshotsTableTableManager get librarySnapshots => + $$LibrarySnapshotsTableTableManager(_db, _db.librarySnapshots); + $$ClientsTableTableManager get clients => + $$ClientsTableTableManager(_db, _db.clients); + $$SettingsTableTableManager get settings => + $$SettingsTableTableManager(_db, _db.settings); +} diff --git a/packages/doudou_server/lib/src/storage/tables.dart b/packages/doudou_server/lib/src/storage/tables.dart new file mode 100644 index 000000000..e1eabef48 --- /dev/null +++ b/packages/doudou_server/lib/src/storage/tables.dart @@ -0,0 +1,54 @@ +import 'package:drift/drift.dart'; + +/// A music server that a doudou client has registered with this doudou-server. +/// Only the URL and display metadata are stored here. Credentials never leave +/// the client that owns them. +@DataClassName('MusicServerRow') +class MusicServers extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + TextColumn get type => text()(); + TextColumn get url => text()(); + BoolColumn get isDefault => boolean().withDefault(const Constant(false))(); + DateTimeColumn get updatedAt => dateTime()(); + + @override + Set get primaryKey => {id}; +} + +/// Per-library snapshots keyed by (musicServerId, kind). The payload is the +/// JSON-encoded library as the client last pushed it. The server treats this +/// as the source of truth and hands it back to any client that asks. +@DataClassName('LibrarySnapshotRow') +class LibrarySnapshots extends Table { + TextColumn get musicServerId => text()(); + TextColumn get kind => text()(); + TextColumn get payload => text()(); + IntColumn get version => integer().withDefault(const Constant(0))(); + DateTimeColumn get updatedAt => dateTime()(); + + @override + Set get primaryKey => {musicServerId, kind}; +} + +/// A doudou client that has authenticated with this server at least once. +@DataClassName('ClientRow') +class Clients extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get firstSeen => dateTime()(); + DateTimeColumn get lastSeen => dateTime()(); + + @override + Set get primaryKey => {id}; +} + +/// Key/value bag for server settings (shared password hash, listen host/port). +@DataClassName('SettingRow') +class Settings extends Table { + TextColumn get key => text()(); + TextColumn get value => text()(); + + @override + Set get primaryKey => {key}; +} diff --git a/packages/doudou_server/linux/.gitignore b/packages/doudou_server/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/packages/doudou_server/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/doudou_server/linux/CMakeLists.txt b/packages/doudou_server/linux/CMakeLists.txt new file mode 100644 index 000000000..be4696d3d --- /dev/null +++ b/packages/doudou_server/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "doudou_server") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "ink.openlyst.doudou_server") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/doudou_server/linux/flutter/CMakeLists.txt b/packages/doudou_server/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/packages/doudou_server/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/doudou_server/linux/flutter/generated_plugin_registrant.h b/packages/doudou_server/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/packages/doudou_server/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/doudou_server/linux/runner/CMakeLists.txt b/packages/doudou_server/linux/runner/CMakeLists.txt new file mode 100644 index 000000000..e97dabc70 --- /dev/null +++ b/packages/doudou_server/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/packages/doudou_server/linux/runner/main.cc b/packages/doudou_server/linux/runner/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/packages/doudou_server/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/doudou_server/linux/runner/my_application.cc b/packages/doudou_server/linux/runner/my_application.cc new file mode 100644 index 000000000..fe819d5a2 --- /dev/null +++ b/packages/doudou_server/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "doudou_server"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "doudou_server"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/doudou_server/linux/runner/my_application.h b/packages/doudou_server/linux/runner/my_application.h new file mode 100644 index 000000000..db16367a7 --- /dev/null +++ b/packages/doudou_server/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/doudou_server/macos/.gitignore b/packages/doudou_server/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/packages/doudou_server/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/doudou_server/macos/Flutter/Flutter-Debug.xcconfig b/packages/doudou_server/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..c2efd0b60 --- /dev/null +++ b/packages/doudou_server/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/doudou_server/macos/Flutter/Flutter-Release.xcconfig b/packages/doudou_server/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..c2efd0b60 --- /dev/null +++ b/packages/doudou_server/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/doudou_server/macos/Runner.xcodeproj/project.pbxproj b/packages/doudou_server/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..e936d8931 --- /dev/null +++ b/packages/doudou_server/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* doudou_server.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "doudou_server.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* doudou_server.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* doudou_server.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ink.openlyst.doudouServer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doudou_server.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doudou_server"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ink.openlyst.doudouServer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doudou_server.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doudou_server"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ink.openlyst.doudouServer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doudou_server.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doudou_server"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/doudou_server/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/doudou_server/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/doudou_server/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/doudou_server/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/doudou_server/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..c719bf670 --- /dev/null +++ b/packages/doudou_server/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/doudou_server/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/doudou_server/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/packages/doudou_server/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/doudou_server/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/doudou_server/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/doudou_server/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/doudou_server/macos/Runner/AppDelegate.swift b/packages/doudou_server/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..b3c176141 --- /dev/null +++ b/packages/doudou_server/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/packages/doudou_server/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/doudou_server/macos/Runner/Base.lproj/MainMenu.xib b/packages/doudou_server/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/packages/doudou_server/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/doudou_server/macos/Runner/Configs/AppInfo.xcconfig b/packages/doudou_server/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..e9da6582a --- /dev/null +++ b/packages/doudou_server/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = doudou_server + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = ink.openlyst.doudouServer + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 ink.openlyst. All rights reserved. diff --git a/packages/doudou_server/macos/Runner/Configs/Debug.xcconfig b/packages/doudou_server/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/packages/doudou_server/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/doudou_server/macos/Runner/Configs/Release.xcconfig b/packages/doudou_server/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/packages/doudou_server/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/doudou_server/macos/Runner/Configs/Warnings.xcconfig b/packages/doudou_server/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/packages/doudou_server/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/doudou_server/macos/Runner/DebugProfile.entitlements b/packages/doudou_server/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..08c3ab17c --- /dev/null +++ b/packages/doudou_server/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/packages/doudou_server/macos/Runner/Info.plist b/packages/doudou_server/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/packages/doudou_server/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/doudou_server/macos/Runner/MainFlutterWindow.swift b/packages/doudou_server/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..34ca70d86 --- /dev/null +++ b/packages/doudou_server/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,21 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: false) + + RegisterGeneratedPlugins(registry: flutterViewController) + + // doudou-server is a headless CLI. Keep the window off-screen and + // invisible; the Dart side never mounts a widget tree and exits when + // the requested CLI command finishes. + self.setIsVisible(false) + self.canHide = true + + super.awakeFromNib() + } +} diff --git a/packages/doudou_server/macos/Runner/Release.entitlements b/packages/doudou_server/macos/Runner/Release.entitlements new file mode 100644 index 000000000..64cabb4e5 --- /dev/null +++ b/packages/doudou_server/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/packages/doudou_server/macos/RunnerTests/RunnerTests.swift b/packages/doudou_server/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..61f3bd1fc --- /dev/null +++ b/packages/doudou_server/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/doudou_server/pubspec.lock b/packages/doudou_server/pubspec.lock new file mode 100644 index 000000000..b91460f22 --- /dev/null +++ b/packages/doudou_server/pubspec.lock @@ -0,0 +1,717 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b + url: "https://pub.dev" + source: hosted + version: "100.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + args: + dependency: "direct main" + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + build_config: + dependency: transitive + description: + name: build_config + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae + url: "https://pub.dev" + source: hosted + version: "1.3.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + url: "https://pub.dev" + source: hosted + version: "4.1.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: "5909d2c6b66817222779e1eedc19e0e28b76d1df7bd9856a4792ccb9881df358" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" + url: "https://pub.dev" + source: hosted + version: "3.1.9" + drift: + dependency: "direct main" + description: + name: drift + sha256: "84688491040b0ceb26575709a84d701c84ad4df4d8aff020adf6d85f155fb0dd" + url: "https://pub.dev" + source: hosted + version: "2.34.2" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "5c0e6ae3c2afcfc2e3f66ea51e7268622906a1e29fd0ee82e6a9b17bfd73a9b0" + url: "https://pub.dev" + source: hosted + version: "2.34.4" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72 + url: "https://pub.dev" + source: hosted + version: "0.19.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: c73fd75df1332d76a6257f4823ae4df9c791f522b97e4a60cbcad214de1becf4 + url: "https://pub.dev" + source: hosted + version: "3.5.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad + url: "https://pub.dev" + source: hosted + version: "0.5.42" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d" + url: "https://pub.dev" + source: hosted + version: "0.45.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.38.4" diff --git a/packages/doudou_server/pubspec.yaml b/packages/doudou_server/pubspec.yaml new file mode 100644 index 000000000..5f8ac6a5e --- /dev/null +++ b/packages/doudou_server/pubspec.yaml @@ -0,0 +1,30 @@ +name: doudou_server +description: Headless sync server for doudou. Holds library snapshots and music server URLs so multiple doudou clients can stay in sync without exposing media server credentials. +publish_to: "none" +version: 1.0.0+1 + +environment: + sdk: ^3.12.2 + +dependencies: + flutter: + sdk: flutter + + args: ^2.5.0 + crypto: ^3.0.6 + drift: ^2.21.0 + sqlite3_flutter_libs: ^0.5.24 + path_provider: ^2.1.5 + path: ^1.9.0 + uuid: ^4.5.1 + intl: ^0.20.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + drift_dev: ^2.21.0 + build_runner: ^2.4.13 + +flutter: + uses-material-design: false diff --git a/packages/doudou_server/windows/.gitignore b/packages/doudou_server/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/packages/doudou_server/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/doudou_server/windows/CMakeLists.txt b/packages/doudou_server/windows/CMakeLists.txt new file mode 100644 index 000000000..de28168ee --- /dev/null +++ b/packages/doudou_server/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(doudou_server LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "doudou_server") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/doudou_server/windows/flutter/CMakeLists.txt b/packages/doudou_server/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..903f4899d --- /dev/null +++ b/packages/doudou_server/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/doudou_server/windows/runner/CMakeLists.txt b/packages/doudou_server/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..394917c05 --- /dev/null +++ b/packages/doudou_server/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/doudou_server/windows/runner/Runner.rc b/packages/doudou_server/windows/runner/Runner.rc new file mode 100644 index 000000000..cac0db7be --- /dev/null +++ b/packages/doudou_server/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "ink.openlyst" "\0" + VALUE "FileDescription", "doudou_server" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "doudou_server" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 ink.openlyst. All rights reserved." "\0" + VALUE "OriginalFilename", "doudou_server.exe" "\0" + VALUE "ProductName", "doudou_server" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/doudou_server/windows/runner/flutter_window.cpp b/packages/doudou_server/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..e3e6fa442 --- /dev/null +++ b/packages/doudou_server/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + // doudou-server is a headless CLI. The Dart side never mounts a widget + // tree, so we intentionally do not show the window on the next frame. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/doudou_server/windows/runner/flutter_window.h b/packages/doudou_server/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/packages/doudou_server/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/doudou_server/windows/runner/main.cpp b/packages/doudou_server/windows/runner/main.cpp new file mode 100644 index 000000000..937ed3425 --- /dev/null +++ b/packages/doudou_server/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"doudou_server", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/doudou_server/windows/runner/resource.h b/packages/doudou_server/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/packages/doudou_server/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/doudou_server/windows/runner/resources/app_icon.ico b/packages/doudou_server/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/packages/doudou_server/windows/runner/resources/app_icon.ico differ diff --git a/packages/doudou_server/windows/runner/runner.exe.manifest b/packages/doudou_server/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..153653e8d --- /dev/null +++ b/packages/doudou_server/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/packages/doudou_server/windows/runner/utils.cpp b/packages/doudou_server/windows/runner/utils.cpp new file mode 100644 index 000000000..3cb714665 --- /dev/null +++ b/packages/doudou_server/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/doudou_server/windows/runner/utils.h b/packages/doudou_server/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/packages/doudou_server/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/doudou_server/windows/runner/win32_window.cpp b/packages/doudou_server/windows/runner/win32_window.cpp new file mode 100644 index 000000000..60608d0fe --- /dev/null +++ b/packages/doudou_server/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/doudou_server/windows/runner/win32_window.h b/packages/doudou_server/windows/runner/win32_window.h new file mode 100644 index 000000000..e901dde68 --- /dev/null +++ b/packages/doudou_server/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/pubspec.lock b/pubspec.lock index 12f3fd656..c73f771b9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -218,7 +218,7 @@ packages: source: hosted version: "0.3.5+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf @@ -467,6 +467,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" + url: "https://pub.dev" + source: hosted + version: "10.3.1" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_slidable: dependency: "direct main" description: @@ -1332,13 +1380,13 @@ packages: source: hosted version: "3.1.5" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5ffe5dcd8..b48a381d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,9 @@ dependencies: wear_plus: ^1.2.5 wearable_rotary: ^2.0.4 watch_connectivity: ^0.2.9 + flutter_secure_storage: ^10.3.1 + uuid: ^4.6.0 + crypto: ^3.0.7 dev_dependencies: flutter_launcher_icons: ^0.14.3 # The "flutter_lints" package below contains a set of recommended lints to diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b5b3735c6..3702d6899 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); FlutterAcrylicPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 17aded7d4..02b665468 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links flutter_acrylic + flutter_secure_storage_windows media_kit_libs_windows_audio permission_handler_windows screen_retriever_windows