diff --git a/.gitignore b/.gitignore index f4aceb61..a94e6996 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ ARCHIVE ignore pubspec.yaml.* -!templates/solidpod/pubspec.yaml.tmpl qtest_*.txt diff --git a/.pubignore b/.pubignore index 9bf863b4..a7371d3c 100644 --- a/.pubignore +++ b/.pubignore @@ -15,8 +15,6 @@ ARCHIVE ignore pubspec.yaml.* -!templates/solidpod/pubspec.yaml.tmpl - .flutter-plugins* qtest_*.txt diff --git a/README.md b/README.md index 5bbc2fe7..1c27dab6 100644 --- a/README.md +++ b/README.md @@ -92,152 +92,6 @@ of the [solidpod](https://github.com/anusii/solidpod) repository. -## Create a new app from the template - -`solidpod` ships with an app template — a ready-to-run Pod file browser, -complete with a navigation rail and a status bar, built on -[`solidui`](https://pub.dev/packages/solidui). It is the practical equivalent -of a `flutter create --template=solidpod`, which stock Flutter cannot offer -because the `--template` flag only accepts a fixed set of built-in types. We -provide a small generator instead. - -The recommended way is to activate the generator once and then run it from any -directory: - -```bash -flutter pub global activate solidpod -solidpod create my_pod_app -``` - -Alternatively, `dart run solidpod:create` works **only from within a package -that already depends on `solidpod`** (for example a clone of the `solidpod` -repository), because `dart run` must resolve the `solidpod:create` executable -through that project's `pubspec.yaml`: - -```bash -dart run solidpod:create my_pod_app -``` - -Running `dart run solidpod:create` from an unrelated directory fails with -`Found no pubspec.yaml file in or parent directories` — use the global -activation above instead. - -### Running from a local checkout (before publishing) - -If you are working on a branch of `solidpod` that is not yet published to -pub.dev, you can still generate an app from any directory without publishing. -Replace `/path/to/solidpod` below with the path to your local checkout. - -- Run the generator script directly. This always uses your current working tree - — both `bin/create.dart` and the template files — so it picks up your edits - on every run: - - ```bash - dart run /path/to/solidpod/bin/create.dart my_pod_app - ``` - -- Or activate the local checkout and use the short `solidpod create` command - anywhere: - - ```bash - flutter pub global activate --source path /path/to/solidpod - solidpod create my_pod_app - ``` - - Note that this takes a snapshot of `bin/create.dart`, so re-run the - `activate` command after editing the generator itself; edits to the template - files are picked up without re-activating. - -For Windows users, command `solidpod` can be added to the system by the -following steps: - -- Run **PowerShell** -- Run the following commands: - - ```shell - $pubCacheBin = "$env:LOCALAPPDATA\Pub\Cache\bin" - $userPath = [Environment]::GetEnvironmentVariable("Path", "User") - - if ($userPath -notlike "*$pubCacheBin*") { - [Environment]::SetEnvironmentVariable("Path", "$userPath;$pubCacheBin", "User") - } - ``` - -- Close **Powershell** -- Open **Command Prompter** or **PowerShell** and use `solidpod` command. - -The generator runs `flutter create` to lay down the platform folders, overlays -the template (substituting your app name), and runs `flutter pub get`. Useful -options: - -| Option | Description | -|-----------------------|------------------------------------------------------| -| `--org ` | Reverse-domain org id (default: `com.example`). | -| `--title ` | Window title shown by the app. | -| `--description `| `pubspec` description. | -| `-o, --output ` | Output directory (default: the app name). | -| `--no-flutter-create` | Render template only; skip platform folders. | -| `--no-pub-get` | Skip the final `flutter pub get`. | - -The generated app starts at a `SolidLogin` screen and, once signed in, shows a -`SolidScaffold` with a home page, an app-files browser and a whole-POD browser. - -### Enabling login (Solid-OIDC client registration) - -Before login will work you must publish a Client Identifier Document for the -app. The generator writes a ready-to-deploy copy of it, together with the web -redirect helper, into the generated project's `solid/` folder: - -- `solid/client-profile.jsonld` — the Solid-OIDC Client Identifier Document. Its - `redirect_uris` are generated to match, byte for byte, the `redirectUris` - passed to `SolidLogin` in `lib/app.dart`. -- `solid/redirect.html` — the web and post-logout redirect helper used by the - `oidc` package. - -Two points are worth understanding: - -- `client-profile.jsonld` is **not** a file the app creates on your POD, and it - cannot be — the app is not yet authenticated at login time. It must already be - hosted, and be publicly readable, at the URL given as the `clientId`. During - login the identity provider fetches that URL to learn which `redirect_uris` - are permitted; if it is missing (HTTP 404) the provider refuses to hand - control back to the app after the consent screen, and login fails with an - `ASWebAuthenticationSession Code=1` (cancelled) error. -- The POD data folders (for example `/data` and `/sharing`) are - created by solidpod's `generateDefaultFolders()` **after** a successful login. - If they have not appeared, it is because login has not completed — that is a - symptom of the missing client profile, not the cause. - -To enable login, publish both files at the location your `clientId` points to. -If you maintain the Solid server — for example the Australian Solid Community -(`solidcommunity.au`) — deploy them alongside the other apps exactly as -`filepod` does: - -```console -https://solidcommunity.au/apps/my_pod_app/client-profile.jsonld -https://solidcommunity.au/apps/my_pod_app/redirect.html -``` - -Then confirm the document is reachable (a public `200`, requiring no -authentication): - -```bash -curl -I https://solidcommunity.au/apps/my_pod_app/client-profile.jsonld -``` - -Once it returns `200`, run `flutter run` and the login redirect will complete -(`filepod`'s own document returns `200`, which is why it can sign in). -Otherwise, host the two files at any public URL you control and update the -`clientId` — and the matching `redirect.html` entry in `redirectUris` — in -`lib/app.dart` accordingly. Note that only the custom redirect **scheme** -(`.://redirect`, e.g. `com.example.mypodapp`) -drops the underscores from the project name, because a URI scheme may not -contain them; every other identifier keeps the project name as-is. - -After generating, also review the remaining placeholders — the `clientId`, -`redirectUris` and `link` in `lib/app.dart`, and the constants in -`lib/constants/app.dart` — and update them for your own deployment. - ## Prerequisites If the package is being used to build either a `macos` or `web` app, diff --git a/bin/create.dart b/bin/create.dart deleted file mode 100644 index 6261fbc3..00000000 --- a/bin/create.dart +++ /dev/null @@ -1,591 +0,0 @@ -/// solidpod:create - scaffold a new Solid Pod file-browser app. -/// -/// This generator stamps out a ready-to-run Flutter application from the -/// `solidpod` app template (a Pod file browser, with navigation rail and status -/// bar, built on `solidui`). It is the practical equivalent of a -/// `flutter create --template=solidpod`, which stock Flutter does not support -/// because the `--template` flag only accepts a fixed set of built-in types. -/// -/// Usage: -/// -/// dart run solidpod:create APP_NAME [options] -/// -/// Run with `--help` for the full list of options. -/// -/// This script deliberately imports only `dart:` libraries so it stays a pure -/// Dart executable that can also be globally activated with -/// `flutter pub global activate`. - -library; - -import 'dart:io'; -import 'dart:isolate'; - -// File extensions copied verbatim (never treated as text for token -// substitution). - -const _binaryExtensions = { - '.png', - '.jpg', - '.jpeg', - '.gif', - '.ico', - '.webp', -}; - -// Operating-system and editor cruft that must never be copied into a generated -// project (and would otherwise break the text decoder). - -const _junkFiles = { - '.DS_Store', - 'Thumbs.db', -}; - -// Dart reserved words that cannot be used as a package name. - -const _reservedWords = { - 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch', - 'class', 'const', 'continue', 'covariant', 'default', 'deferred', 'do', - 'dynamic', 'else', 'enum', 'export', 'extends', 'extension', 'external', - 'factory', 'false', 'final', 'finally', 'for', 'function', 'get', 'hide', - 'if', 'implements', 'import', 'in', 'interface', 'is', 'library', 'mixin', - 'new', 'null', 'on', 'operator', 'part', 'rethrow', 'return', 'set', 'show', - 'static', 'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', - 'typedef', 'var', 'void', 'while', 'with', 'yield', - // Names that would clash with the Flutter toolchain. - 'flutter', 'test', -}; - -Future main(List arguments) async { - final args = _Args.parse(arguments); - - if (args.help) { - _printUsage(stdout); - return; - } - - final projectName = args.projectName; - if (projectName == null) { - stderr.writeln('Error: missing .\n'); - _printUsage(stderr); - exitCode = 64; // EX_USAGE - return; - } - - final nameError = _validateProjectName(projectName); - if (nameError != null) { - stderr.writeln('Error: $nameError'); - exitCode = 64; - return; - } - - // Derive the human-facing names from the package name unless overridden. - - final appName = _pascalCase(projectName); - final appTitle = args.title ?? '$appName - File Browser for Solid Pods'; - final appDescription = args.description ?? - '$appName - manage files on your personal online data store (POD).'; - final orgName = args.org; - - // The project name is used as-is almost everywhere (package name, imports, - // applicationId, POD folder, and the clientId / redirect URL paths, which all - // permit underscores). The ONE exception is the custom redirect URI scheme: a - // URI scheme may not contain underscores (RFC 3986 allows only letters, - // digits and "+", "-", "."), and an Android intent-filter scheme must be - // lower case. So we derive a scheme name with the underscores stripped, and - // use it only inside the `org.scheme://` redirect. - - final schemeName = projectName.replaceAll('_', ''); - final outputDir = Directory(args.output ?? projectName); - - final templateDir = await _resolveTemplateDir(); - if (templateDir == null || !templateDir.existsSync()) { - stderr.writeln( - 'Error: could not locate the solidpod template directory.\n' - 'Expected it alongside the solidpod package (templates/solidpod/).', - ); - exitCode = 70; // EX_SOFTWARE - return; - } - - final tokens = { - '{{projectName}}': projectName, - '{{appName}}': appName, - '{{appTitle}}': appTitle, - '{{appDescription}}': appDescription, - '{{orgName}}': orgName, - '{{schemeName}}': schemeName, - }; - - stdout.writeln('Creating Solid Pod app "$appName" in ${outputDir.path}/ ...'); - - // Step 1: let `flutter create` lay down the platform folders and tooling - // (android/, ios/, etc.) unless the caller opted out. We pass the project - // name and org so the generated metadata matches our overlaid pubspec. - - if (args.runFlutterCreate) { - final created = await _runFlutterCreate( - projectName: projectName, - org: orgName, - output: outputDir, - ); - if (!created) { - exitCode = 70; - return; - } - } else { - outputDir.createSync(recursive: true); - } - - // Step 2: overlay the template, substituting tokens as we go. This overwrites - // the default main.dart and pubspec.yaml that flutter create produced. - - stdout.writeln('Applying the solidpod template ...'); - _renderTemplate( - source: templateDir, - target: outputDir, - tokens: tokens, - ); - - // Step 3: drop the default counter widget test, which references the - // scaffolding flutter create generated rather than our app. - - final defaultTest = File('${outputDir.path}/test/widget_test.dart'); - if (defaultTest.existsSync()) { - defaultTest.deleteSync(); - } - - // Step 4: wire up the OIDC redirect on the platforms that flutter create does - // not configure (Android manifest placeholder and the iOS URL scheme). The - // macOS entitlements are supplied by the template overlay above. - - if (args.runFlutterCreate) { - _patchAuthRedirect(outputDir, scheme: '$orgName.$schemeName'); - } - - // Step 5: resolve dependencies now that the pubspec lists solidui. - - if (args.runPubGet && args.runFlutterCreate) { - stdout.writeln('Running flutter pub get ...'); - await _runProcess('flutter', ['pub', 'get'], outputDir.path); - } - - _printNextSteps( - stdout, - outputDir.path, - runFlutterCreate: args.runFlutterCreate, - ); -} - -// ── Template rendering ───────────────────────────────────────────────────── - -void _renderTemplate({ - required Directory source, - required Directory target, - required Map tokens, -}) { - final sourcePath = source.path; - for (final entity in source.listSync(recursive: true)) { - if (entity is! File) continue; - - final name = entity.uri.pathSegments.last; - - // Skip operating-system and editor junk that may sit alongside the template - // (e.g. macOS .DS_Store), so it never lands in the generated project nor - // trips up the text decoder below. - - if (_junkFiles.contains(name)) continue; - - // Path of the file relative to the template root. - - var relative = entity.path.substring(sourcePath.length); - relative = relative.replaceFirst(RegExp(r'^[\\/]+'), ''); - - // Strip the .tmpl marker and substitute any tokens that appear in the path - // itself (so template authors can parameterise file names too). - - if (relative.endsWith('.tmpl')) { - relative = relative.substring(0, relative.length - '.tmpl'.length); - } - relative = _substitute(relative, tokens); - - final destination = File('${target.path}/$relative'); - destination.parent.createSync(recursive: true); - - // Known-binary files are copied verbatim. Anything else we attempt to read - // as text for token substitution, falling back to a byte copy if it turns - // out not to be valid UTF-8 (so an unexpected binary never crashes us). - - if (_isBinary(relative)) { - destination.writeAsBytesSync(entity.readAsBytesSync()); - continue; - } - try { - final rendered = _substitute(entity.readAsStringSync(), tokens); - destination.writeAsStringSync(rendered); - } on FileSystemException { - destination.writeAsBytesSync(entity.readAsBytesSync()); - } - } -} - -String _substitute(String input, Map tokens) { - var output = input; - tokens.forEach((token, value) { - output = output.replaceAll(token, value); - }); - return output; -} - -bool _isBinary(String path) { - final dot = path.lastIndexOf('.'); - if (dot < 0) return false; - return _binaryExtensions.contains(path.substring(dot).toLowerCase()); -} - -// ── OIDC redirect wiring ─────────────────────────────────────────────────── - -// flutter create does not register the custom redirect scheme that the OIDC -// login needs. We add the Android manifest placeholder and the iOS URL scheme -// here; the macOS network-client/keychain entitlements come from the template. - -void _patchAuthRedirect(Directory output, {required String scheme}) { - _patchAndroidRedirectScheme(output, scheme); - _patchIosUrlScheme(output, scheme); -} - -void _patchAndroidRedirectScheme(Directory output, String scheme) { - // flutter_appauth reads `appAuthRedirectScheme` from the manifest placeholders - // declared in the app-level Gradle file (Kotlin or Groovy DSL). - - final placeholder = 'appAuthRedirectScheme'; - - final kts = File('${output.path}/android/app/build.gradle.kts'); - if (kts.existsSync()) { - final content = kts.readAsStringSync(); - if (content.contains(placeholder)) return; - final patched = content.replaceFirstMapped( - RegExp(r'versionName = flutter\.versionName\n'), - (m) => '${m[0]} manifestPlaceholders.putAll(mapOf(\n' - ' "$placeholder" to "$scheme",\n' - ' ))\n', - ); - if (patched != content) { - kts.writeAsStringSync(patched); - } else { - stderr.writeln( - 'Warning: could not add $placeholder to build.gradle.kts; add ' - 'manifestPlaceholders["$placeholder"] = "$scheme" to defaultConfig ' - 'manually.', - ); - } - return; - } - - final groovy = File('${output.path}/android/app/build.gradle'); - if (groovy.existsSync()) { - final content = groovy.readAsStringSync(); - if (content.contains(placeholder)) return; - final patched = content.replaceFirstMapped( - RegExp(r'versionName flutterVersionName\n'), - (m) => - '${m[0]} manifestPlaceholders = [$placeholder: "$scheme"]\n', - ); - if (patched != content) { - groovy.writeAsStringSync(patched); - } else { - stderr.writeln( - 'Warning: could not add $placeholder to build.gradle; add ' - 'manifestPlaceholders = [$placeholder: "$scheme"] to defaultConfig ' - 'manually.', - ); - } - } -} - -void _patchIosUrlScheme(Directory output, String scheme) { - final plist = File('${output.path}/ios/Runner/Info.plist'); - if (!plist.existsSync()) return; - - final content = plist.readAsStringSync(); - if (content.contains('CFBundleURLTypes')) return; - - // Register the custom scheme so iOS can return to the app after login. - - final block = ''' - CFBundleURLTypes - - - CFBundleURLSchemes - - $scheme - - - -'''; - - final patched = content.replaceFirst( - RegExp(r'\n\n'), - '\n$block\n', - ); - if (patched != content) { - plist.writeAsStringSync(patched); - } else { - stderr.writeln( - 'Warning: could not add CFBundleURLTypes to ios/Runner/Info.plist; add ' - 'the "$scheme" URL scheme manually.', - ); - } -} - -// ── Template location ────────────────────────────────────────────────────── - -Future _resolveTemplateDir() async { - // Preferred: resolve through the package config so this works whether invoked - // as `dart run solidpod:create` or after a global activation. - - try { - final libUri = await Isolate.resolvePackageUri( - Uri.parse('package:solidpod/solidpod.dart'), - ); - if (libUri != null) { - final dir = Directory.fromUri(libUri.resolve('../templates/solidpod/')); - if (dir.existsSync()) return dir; - } - } catch (_) { - // Fall through to the script-relative lookup below. - } - - // Fallback: relative to this script (bin/create.dart -> ../templates/...). - - try { - final dir = Directory.fromUri( - Platform.script.resolve('../templates/solidpod/'), - ); - if (dir.existsSync()) return dir; - } catch (_) { - // Ignored - caller handles the null result. - } - - return null; -} - -// ── Process helpers ──────────────────────────────────────────────────────── - -Future _runFlutterCreate({ - required String projectName, - required String org, - required Directory output, -}) async { - stdout.writeln('Running flutter create ...'); - return _runProcess( - 'flutter', - [ - 'create', - '--project-name', - projectName, - '--org', - org, - '--no-pub', - output.path, - ], - null, - ); -} - -Future _runProcess( - String executable, - List args, - String? workingDirectory, -) async { - try { - final process = await Process.start( - executable, - args, - workingDirectory: workingDirectory, - mode: ProcessStartMode.inheritStdio, - runInShell: true, - ); - final code = await process.exitCode; - if (code != 0) { - stderr - .writeln('Error: `$executable ${args.join(' ')}` exited with $code.'); - return false; - } - return true; - } on ProcessException catch (e) { - stderr.writeln( - 'Error: could not run `$executable`. Is it installed and on your PATH?\n' - ' ${e.message}', - ); - return false; - } -} - -// ── Naming helpers ───────────────────────────────────────────────────────── - -String? _validateProjectName(String name) { - if (!RegExp(r'^[a-z][a-z0-9_]*$').hasMatch(name)) { - return '"$name" is not a valid package name. Use lowercase letters, ' - 'digits and underscores, starting with a letter ' - '(e.g. my_pod_app).'; - } - if (_reservedWords.contains(name)) { - return '"$name" is a reserved word and cannot be used as a package name.'; - } - return null; -} - -String _pascalCase(String snake) { - return snake - .split('_') - .where((part) => part.isNotEmpty) - .map((part) => part[0].toUpperCase() + part.substring(1)) - .join(); -} - -// ── Argument parsing ─────────────────────────────────────────────────────── - -class _Args { - _Args({ - required this.projectName, - required this.org, - required this.title, - required this.description, - required this.output, - required this.runFlutterCreate, - required this.runPubGet, - required this.help, - }); - - final String? projectName; - final String org; - final String? title; - final String? description; - final String? output; - final bool runFlutterCreate; - final bool runPubGet; - final bool help; - - static _Args parse(List arguments) { - String? projectName; - var org = 'com.example'; - String? title; - String? description; - String? output; - var runFlutterCreate = true; - var runPubGet = true; - var help = false; - - String valueFor(String arg, String? inlineValue, Iterator it) { - if (inlineValue != null) return inlineValue; - if (it.moveNext()) return it.current; - stderr.writeln('Error: option "$arg" expects a value.'); - exit(64); - } - - final it = arguments.iterator; - while (it.moveNext()) { - final arg = it.current; - - // Allow an optional leading `create` verb so both `solidpod create app` - // and `solidpod app` work after a global activation. - - if (arg == 'create' && projectName == null) continue; - - if (arg == '-h' || arg == '--help') { - help = true; - continue; - } - if (arg == '--no-flutter-create') { - runFlutterCreate = false; - continue; - } - if (arg == '--no-pub-get') { - runPubGet = false; - continue; - } - - String? key = arg; - String? inline; - final eq = arg.indexOf('='); - if (arg.startsWith('--') && eq != -1) { - key = arg.substring(0, eq); - inline = arg.substring(eq + 1); - } - - switch (key) { - case '--org': - org = valueFor(arg, inline, it); - case '--title': - title = valueFor(arg, inline, it); - case '--description': - description = valueFor(arg, inline, it); - case '--output': - case '-o': - output = valueFor(arg, inline, it); - default: - if (arg.startsWith('-')) { - stderr.writeln('Error: unknown option "$arg".'); - exit(64); - } - projectName ??= arg; - } - } - - return _Args( - projectName: projectName, - org: org, - title: title, - description: description, - output: output, - runFlutterCreate: runFlutterCreate, - runPubGet: runPubGet, - help: help, - ); - } -} - -void _printUsage(IOSink out) { - out.writeln(''' -Scaffold a new Solid Pod file-browser app from the solidpod template. - -Usage: - dart run solidpod:create [options] - -Options: - --org Reverse-domain organisation id (default: com.example). - --title Window title (default: " - File Browser for - Solid Pods"). - --description pubspec description. - -o, --output Output directory (default: ). - --no-flutter-create Only render the template; skip running flutter create - (no platform folders are generated). - --no-pub-get Skip the final flutter pub get. - -h, --help Show this help. - -Example: - dart run solidpod:create my_pod_app --org au.org.example -'''); -} - -void _printNextSteps( - IOSink out, - String path, { - required bool runFlutterCreate, -}) { - out.writeln(''' - -Done! Your Solid Pod app is ready in $path/ - -Next steps: - cd $path${runFlutterCreate ? '' : '\n flutter create --project-name . # generate platform folders\n flutter pub get'} - flutter run - -Then update the Solid app registration (clientId, redirectUris, link) in -lib/app.dart and the constants in lib/constants/app.dart for your deployment. - -On macOS/iOS, enable signing once in Xcode (Signing & Capabilities -> Team) -so the keychain-backed login can build. See the generated README for details. -'''); -} diff --git a/pubspec.yaml b/pubspec.yaml index 5c3163ab..b310179a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,18 +3,6 @@ description: Support access to private data from PODs on Solid servers. version: 1.0.11 homepage: https://github.com/anusii/solidpod -# Scaffold a new Solid Pod file-browser app (a pod browser, with navigation -# rail and status bar, built on solidui) from the bundled template: -# -# dart run solidpod:create my_pod_app -# -# Or, after `flutter pub global activate solidpod`: -# -# solidpod create my_pod_app - -executables: - solidpod: create - environment: sdk: '>=3.2.3 <4.0.0' flutter: '>=1.17.0' diff --git a/templates/solidpod/README.md.tmpl b/templates/solidpod/README.md.tmpl deleted file mode 100644 index adfb08a2..00000000 --- a/templates/solidpod/README.md.tmpl +++ /dev/null @@ -1,84 +0,0 @@ -# {{appName}} - -{{appDescription}} - -This app was generated from the [`solidpod`](https://pub.dev/packages/solidpod) -app template — a Solid Pod file browser built with the -[`solidui`](https://pub.dev/packages/solidui) widget set. It comes ready with: - -- a [`SolidLogin`] screen that connects to the user's data vault on their - chosen Solid server; -- a [`SolidScaffold`] with a navigation rail (collapsing to a drawer on narrow - screens) and a status bar showing the server, login and security-key state; -- a [`SolidFile`] browser for both the app's own folder and the whole POD; -- theme switching, an About dialog and an Invite Others action. - -## Getting started - -```bash -flutter pub get -flutter run -``` - -## Next steps - -A few values were filled in with placeholders when this project was generated. -Update them for your own deployment: - -- **Solid app registration** in `lib/app.dart` — the `clientId`, - `redirectUris` and `link` passed to `SolidLogin`. These identify your app to - the Solid server during login. **The `clientId` URL must actually resolve to - a Client Identifier Document (a `client-profile.jsonld`) that lists these - exact `redirectUris`.** Until you publish that document (and list your - `{{orgName}}.{{schemeName}}://redirect` scheme in it), the identity provider has - no client to validate and the login page will not appear — this is the most - common reason a freshly generated app cannot reach the login screen. See the - [Solid-OIDC client identifiers](https://solidproject.org/TR/oidc#clientids) - documentation. -- **App constants** in `lib/constants/app.dart` — the title, hosting URL and - the Invite Others message. -- **Allowed upload types** in `lib/constants/app.dart` — `appUploadConfig` - currently restricts uploads to `.md` and `.txt`; widen `allowedExtensions` - to suit your app. -- **Icons** in `assets/images/` — replace `app_icon.png` and `app_image.jpg`, - then run `dart run flutter_launcher_icons` to regenerate platform icons. - -## Login (OIDC) setup - -> **Required before login works:** publish this app's Client Identifier -> Document. The identity provider fetches your `clientId` URL to learn which -> `redirect_uris` are allowed; if it is missing the login is cancelled after the -> consent screen (`ASWebAuthenticationSession Code=1`). A ready-to-deploy -> `client-profile.jsonld` and `redirect.html` were generated in the -> [`solid/`](solid/) folder — see `solid/README.md` for where to publish them. - -The OIDC redirect is pre-wired so that login works on every platform: - -- **macOS** — `macos/Runner/*.entitlements` grant `network.client` (so the - login web session can open), keychain access (for token storage) and - user-selected file access (for uploads/downloads). -- **Android** — `android/app/build.gradle.kts` sets the `appAuthRedirectScheme` - manifest placeholder. -- **iOS** — `ios/Runner/Info.plist` registers the custom URL scheme. - -Because the macOS/iOS apps now request keychain and sandbox capabilities, you -must enable signing once: open `macos/Runner.xcworkspace` (or -`ios/Runner.xcworkspace`) in Xcode and pick your team under **Signing & -Capabilities**, or run `flutter run` with a configured Apple developer account. - -## Project layout - -``` -lib/ - main.dart Application entry point and desktop window setup. - app.dart Root widget; wraps the app in SolidLogin. - app_scaffold.dart SolidScaffold with the nav bar, status bar and menu. - home.dart Introductory home page. - constants/app.dart App-wide constants (title, upload and invite configs). - screens/ - browse_files.dart Whole-POD file browser (SolidFile at the root). -solid/ - client-profile.jsonld Solid-OIDC client document to publish (see Login setup). - redirect.html Web/post-logout redirect helper to publish alongside it. - README.md Where and how to publish the two files above. -``` diff --git a/templates/solidpod/analysis_options.yaml b/templates/solidpod/analysis_options.yaml deleted file mode 100644 index 42239ef8..00000000 --- a/templates/solidpod/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Configure the analyzer for static analysis of Dart code. -# -# Add the following to pubspec.yaml: -# -# dev_dependencies: -# flutter_lints: ^6.0.0 -# -# Invoke the analyzer from the command line: -# -# flutter analyze - -# Activate recommended lints for Flutter apps, packages, and plugins -# designed to encourage good coding practices. - -include: package:flutter_lints/flutter.yaml - -linter: - rules: - avoid_print: true - prefer_single_quotes: true - require_trailing_commas: true - directives_ordering: false - prefer_const_constructors: true - -# Identify directories to ignore. - -analyzer: - exclude: - - ignore/** - - ignore/ diff --git a/templates/solidpod/assets/images/app_icon.png b/templates/solidpod/assets/images/app_icon.png deleted file mode 100644 index 7d08189e..00000000 Binary files a/templates/solidpod/assets/images/app_icon.png and /dev/null differ diff --git a/templates/solidpod/assets/images/app_image.jpg b/templates/solidpod/assets/images/app_image.jpg deleted file mode 100644 index 2b292091..00000000 Binary files a/templates/solidpod/assets/images/app_image.jpg and /dev/null differ diff --git a/templates/solidpod/lib/app.dart.tmpl b/templates/solidpod/lib/app.dart.tmpl deleted file mode 100644 index d9064a28..00000000 --- a/templates/solidpod/lib/app.dart.tmpl +++ /dev/null @@ -1,70 +0,0 @@ -/// {{appName}} - orchestrate the primary login widget. -/// -/// This file was generated from the `solidpod` app template -/// (`dart run solidpod:create`). Edit it freely to suit your app. - -library; - -import 'package:flutter/material.dart'; - -import 'package:solidui/solidui.dart'; - -import 'package:{{projectName}}/app_scaffold.dart'; -import 'package:{{projectName}}/constants/app.dart'; - -// This widget is the root of the application. On startup it calls upon -// [SolidLogin] to connect to the user's Pod stored within their data vault on -// their chosen Solid server. - -class App extends StatelessWidget { - const App({super.key}); - - @override - Widget build(BuildContext context) { - return SolidThemeApp( - // We can manually turn off the debug banner. It is turned off - // automatically for a `flutter --release`. - - debugShowCheckedModeBanner: false, - - title: appTitle, - - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF007AFF), - ), - useMaterial3: true, - ), - - home: SolidLogin( - title: appTitle.replaceAll(' - ', '\n'), - image: const AssetImage('assets/images/app_image.jpg'), - logo: const AssetImage('assets/images/app_icon.png'), - - // The application folder created on the user's POD. - - appDirectory: '{{projectName}}', - - // TODO Update the following Solid app registration details to point at - // your own deployment. They identify your app to the Solid server - // during login, and the clientId MUST resolve to a client profile - // document that lists these exact redirectUris (see the solid/ folder). - // See https://solidproject.org for more information. - // - // Note: the custom redirect scheme drops underscores from the project - // name ('{{orgName}}.{{schemeName}}'), because a URI scheme may not - // contain underscores. Every other identifier keeps the project name. - - link: 'https://github.com/example/{{projectName}}', - clientId: - 'https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld', - redirectUris: [ - 'https://solidcommunity.au/apps/{{projectName}}/redirect.html', - '{{orgName}}.{{schemeName}}://redirect', - 'http://localhost:4400/redirect', - ], - child: appScaffold, - ), - ); - } -} diff --git a/templates/solidpod/lib/app_scaffold.dart.tmpl b/templates/solidpod/lib/app_scaffold.dart.tmpl deleted file mode 100644 index 6bda56cd..00000000 --- a/templates/solidpod/lib/app_scaffold.dart.tmpl +++ /dev/null @@ -1,145 +0,0 @@ -/// {{appName}} - the primary application scaffold. -/// -/// This file was generated from the `solidpod` app template -/// (`dart run solidpod:create`). Edit it freely to suit your app. - -library; - -import 'package:flutter/material.dart'; - -import 'package:solidui/solidui.dart'; - -import 'package:{{projectName}}/constants/app.dart'; -import 'package:{{projectName}}/home.dart'; -import 'package:{{projectName}}/screens/browse_files.dart'; - -final _scaffoldController = SolidScaffoldController(); - -const appScaffold = AppScaffold(); - -class AppScaffold extends StatelessWidget { - const AppScaffold({super.key}); - - @override - Widget build(BuildContext context) { - return SolidScaffold( - controller: _scaffoldController, - hideNavRail: false, - enableProfile: true, - onLogout: (context) => SolidAuthHandler.instance.handleLogout(context), - - // The navigation menu drives the side navigation rail (and the drawer on - // narrow screens). Each entry exposes a top-level page of the app. - - menu: const [ - SolidMenuItem( - icon: Icons.home, - title: 'Home', - tooltip: ''' - - **Home** - - Tap here to return to the main page for the app. - - ''', - child: Home(title: appTitle), - ), - SolidMenuItem( - icon: Icons.folder, - title: 'App Files', - tooltip: ''' - - **Files** - - Tap here to browse the files on your POD for this app. - - ''', - child: SolidFile(uploadConfig: appUploadConfig), - ), - SolidMenuItem( - icon: Icons.storage, - title: 'All POD Files', - tooltip: ''' - - **All Files** - - Tap here to browse all folders on your POD from the root. - - ''', - child: BrowseFiles(), - ), - ], - appBar: SolidAppBarConfig( - title: appTitle.split(' - ')[0], - versionConfig: const SolidVersionConfig( - changelogUrl: 'https://github.com/example/{{projectName}}/blob/dev/' - 'CHANGELOG.md', - showUpdateButton: true, - downloadUrl: 'https://solidcommunity.au/installers/', - ), - actions: [ - SolidAppBarAction( - icon: Icons.folder, - onPressed: () => _scaffoldController.navigateToSubpage( - const SolidFile(uploadConfig: appUploadConfig), - ), - tooltip: 'Files', - ), - ], - ), - - // The status bar runs along the bottom of the window, surfacing the - // current server, login state and security key status. - - statusBar: const SolidStatusBarConfig( - serverInfo: SolidServerInfo(serverUri: SolidConfig.defaultServerUrl), - loginStatus: SolidLoginStatus(), - securityKeyStatus: SolidSecurityKeyStatus(), - ), - aboutConfig: SolidAboutConfig( - applicationName: appTitle.split(' - ')[0], - applicationIcon: Image.asset( - 'assets/images/app_icon.png', - width: 64, - height: 64, - ), - applicationLegalese: ''' - - © {{appName}} - - ''', - text: ''' - - {{appName}} is a file browser application that allows you to manage - files on your personal online data store (Pod) hosted on a Solid - server. - - Key features: - - 📂 Browse and manage files on your Solid POD; - - 📤 Upload files to your POD; - - 📥 Download files from your POD; - - 🔐 Security key management for encrypted data; - - 🎨 Theme switching (light/dark/system); - - 🧭 Responsive navigation (rail ↔ drawer). - - Built with [solidpod](https://pub.dev/packages/solidpod) and - [solidui](https://pub.dev/packages/solidui) for the - [Australian Solid Community](https://solidcommunity.au). - - ''', - ), - themeToggle: const SolidThemeToggleConfig( - enabled: true, - showInAppBarActions: true, - ), - inviteConfig: inviteOthersConfig, - child: const Home(title: appTitle), - ); - } -} diff --git a/templates/solidpod/lib/constants/app.dart.tmpl b/templates/solidpod/lib/constants/app.dart.tmpl deleted file mode 100644 index c047ca25..00000000 --- a/templates/solidpod/lib/constants/app.dart.tmpl +++ /dev/null @@ -1,59 +0,0 @@ -/// {{appName}} - app-wide constants. -/// -/// This file was generated from the `solidpod` app template -/// (`dart run solidpod:create`). Edit it freely to suit your app. - -library; - -import 'package:solidui/solidui.dart' - show SolidFileUploadConfig, SolidInviteOthersConfig; - -/// Application title displayed as the window title. - -const String appTitle = '{{appTitle}}'; - -/// Shared upload configuration for every `SolidFile` view in {{appName}}. -/// -/// Restricts the file picker (both the toolbar Upload button and the side -/// upload panel) to Markdown and plain text files. Extensions are matched -/// case-insensitively by SolidUI, so users may still pick `.MD` / `.TXT`. -/// Adjust `allowedExtensions` to suit the file types your app manages. - -const SolidFileUploadConfig appUploadConfig = SolidFileUploadConfig( - allowedExtensions: ['md', 'txt'], -); - -/// Public URL where {{appName}} is hosted. Used by the Invite Others -/// feature to send a working link to the recipient. - -const String appUrl = 'https://{{projectName}}.solidcommunity.au/'; - -/// Application-wide Invite Others configuration shared by the -/// AppBar share button and the App Info dialog so that users can -/// invite others to set up their POD and try {{appName}}. - -const SolidInviteOthersConfig inviteOthersConfig = SolidInviteOthersConfig( - applicationName: '{{appName}}', - appUrl: appUrl, - appDescription: - 'manage/share resources hosted on your Solid server using {{appName}}', - messageTemplate: ''' -You might like to try the {appName} app, available online here: - -{appUrl} - -Signing into {appName} will set up your data vault so you can manage and -exchange files privately with other Solid users. - -''', - subject: 'Try the {{appName}} app on your Solid POD', - tooltip: ''' - - **Invite Others** - - Tap to invite someone else to try {{appName}}. You can copy the - invitation to the clipboard or share it through any messaging app - installed on your device. - - ''', -); diff --git a/templates/solidpod/lib/home.dart.tmpl b/templates/solidpod/lib/home.dart.tmpl deleted file mode 100644 index 8cbd4eca..00000000 --- a/templates/solidpod/lib/home.dart.tmpl +++ /dev/null @@ -1,71 +0,0 @@ -/// {{appName}} - the application introductory home page. -/// -/// This file was generated from the `solidpod` app template -/// (`dart run solidpod:create`). Edit it freely to suit your app. - -library; - -import 'package:flutter/material.dart'; - -class Home extends StatefulWidget { - const Home({super.key, required this.title}); - - final String title; - - @override - State createState() => _HomeState(); -} - -class _HomeState extends State { - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Center( - child: Card( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.folder_open, - size: 64, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - widget.title, - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 24), - Text( - ''' -Welcome to {{appName}}! - -{{appName}} is a Solid file browser that lets you manage -files on your personal online data store (POD). - -Key features: - -• Browse files and folders on your Solid POD -• Upload files to your POD -• Download files from your POD -• View all POD files from the root -• Security key management for encrypted data -• Theme switching (light/dark/system) -• Responsive navigation (rail ↔ drawer) - -Use the navigation menu to explore your POD files! - ''', - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/templates/solidpod/lib/main.dart.tmpl b/templates/solidpod/lib/main.dart.tmpl deleted file mode 100644 index d3942a27..00000000 --- a/templates/solidpod/lib/main.dart.tmpl +++ /dev/null @@ -1,64 +0,0 @@ -/// {{appName}} - main entry point for the application. -/// -/// This file was generated from the `solidpod` app template -/// (`dart run solidpod:create`). Edit it freely to suit your app. - -library; - -import 'package:flutter/material.dart'; - -import 'package:solidui/solidui.dart'; -import 'package:window_manager/window_manager.dart'; - -import 'package:{{projectName}}/app.dart'; -import 'package:{{projectName}}/constants/app.dart'; - -// The main entry point for the application. We require [async] here because we -// asynchronously [await] the window manager below. Eventually `main()` hands -// over to the widget passed to [runApp]. - -void main() async { - // Optionally, during development, we can use [debugPrint] to trace - // execution. The output is not shown on a `flutter --release`. To quieten the - // `flutter --debug` output we can globally map [debugPrint] to a no-op: - // - // debugPrint = (String? message, {int? wrapWidth}) { - // null; - // }; - - // ── Desktop setup ────────────────────────────────────────────────────────── - - // We ensure the Flutter bindings are initialised for the async operations - // below, particularly to set the desktop window [title]. - - WidgetsFlutterBinding.ensureInitialized(); - - if (isDesktop) { - await windowManager.ensureInitialized(); - - // For the desktop app we tune various window oriented settings. These are - // not required for mobile apps. - - const windowOptions = WindowOptions( - title: appTitle, - minimumSize: Size(500, 800), - backgroundColor: Colors.transparent, - skipTaskbar: false, - titleBarStyle: TitleBarStyle.normal, - ); - - // Await the window being shown and receiving focus before we run the app. - - await windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }); - } - - // ── Run the app ──────────────────────────────────────────────────────────── - - // The runApp() function takes the given Widget and makes it the root of the - // tree of widgets that the app creates. - - runApp(const App()); -} diff --git a/templates/solidpod/lib/screens/browse_files.dart.tmpl b/templates/solidpod/lib/screens/browse_files.dart.tmpl deleted file mode 100644 index 6693aed9..00000000 --- a/templates/solidpod/lib/screens/browse_files.dart.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -/// {{appName}} - display all folders from the root of a user's pod. -/// -/// This file was generated from the `solidpod` app template -/// (`dart run solidpod:create`). Edit it freely to suit your app. - -library; - -import 'package:flutter/material.dart'; - -import 'package:solidui/solidui.dart'; - -import 'package:{{projectName}}/constants/app.dart'; - -class BrowseFiles extends StatelessWidget { - const BrowseFiles({super.key}); - - @override - Widget build(BuildContext context) { - // SolidFile() from `solidui` is a comprehensive file browser for the - // resources contained in your data vault hosted on any Solid server. - - return const SolidFile( - currentPath: SolidFile.podRoot, - friendlyFolderName: 'All Files and Folders', - uploadConfig: appUploadConfig, - ); - } -} diff --git a/templates/solidpod/macos/Runner/DebugProfile.entitlements b/templates/solidpod/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 4d3b3422..00000000 --- a/templates/solidpod/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,20 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - keychain-access-groups - - com.apple.security.keychain - - com.apple.security.files.user-selected.read-write - - - diff --git a/templates/solidpod/macos/Runner/Release.entitlements b/templates/solidpod/macos/Runner/Release.entitlements deleted file mode 100644 index 4d3b3422..00000000 --- a/templates/solidpod/macos/Runner/Release.entitlements +++ /dev/null @@ -1,20 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - keychain-access-groups - - com.apple.security.keychain - - com.apple.security.files.user-selected.read-write - - - diff --git a/templates/solidpod/pubspec.yaml.tmpl b/templates/solidpod/pubspec.yaml.tmpl deleted file mode 100644 index c265d2d9..00000000 --- a/templates/solidpod/pubspec.yaml.tmpl +++ /dev/null @@ -1,54 +0,0 @@ -name: {{projectName}} -description: {{appDescription}} -publish_to: 'none' -version: 1.0.0+1 - -environment: - sdk: '>=3.2.3 <4.0.0' - flutter: '>=3.10.0' - -# To automatically upgrade package dependencies: -# -# flutter pub upgrade --major-versions -# -# To see which dependencies have newer versions available: -# -# flutter pub outdated - -dependencies: - flutter: - sdk: flutter - solidui: ^1.0.7 - window_manager: ^0.5.1 - -dev_dependencies: - flutter_launcher_icons: ^0.14.4 - flutter_lints: ^6.0.0 - -flutter: - assets: - - assets/images/app_icon.png - - assets/images/app_image.jpg - uses-material-design: true - -# Define launcher icons for all platforms (except Linux) so the app icons can -# be regenerated using: -# -# dart run flutter_launcher_icons - -flutter_launcher_icons: - image_path: "assets/images/app_icon.png" - android: true - min_sdk_android: 21 - ios: true - remove_alpha_ios: true - background_color_ios: "#ffffff" - macos: - generate: true - web: - generate: true - background_color: "#ffffff" - theme_color: "#ffffff" - windows: - generate: true - icon_size: 48 # min:48, max:256, default: 48 diff --git a/templates/solidpod/solid/README.md.tmpl b/templates/solidpod/solid/README.md.tmpl deleted file mode 100644 index 2f30817b..00000000 --- a/templates/solidpod/solid/README.md.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -# Solid-OIDC client registration for {{appName}} - -**Login will not work until the two files in this folder are published on the -public web at the location your `clientId` points to.** This is the single most -common reason a freshly generated Solid app cannot complete login: the identity -provider fetches the `clientId` URL to learn which `redirect_uris` are allowed, -and if that document is missing (HTTP 404) it refuses to hand control back to -the app after the consent screen (you will see an -`ASWebAuthenticationSession Code=1` / cancelled error). - -This folder contains: - -- `client-profile.jsonld` — the Solid-OIDC Client Identifier Document. Its - `redirect_uris` exactly match those passed to `SolidLogin` in `lib/app.dart`. -- `redirect.html` — the web/post-logout redirect helper used by the `oidc` - package (only needed for the web build, but deploy it too). - -## Where to publish - -The app currently uses: - -``` -clientId: https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld -``` - -Publish both files so that they are reachable (HTTP 200, public, no auth) at: - -``` -https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld -https://solidcommunity.au/apps/{{projectName}}/redirect.html -``` - -- **If you maintain solidcommunity.au** (the ANU Solid Community), deploy these - to `apps/{{projectName}}/` alongside the other apps (this is how `filepod` is set - up). -- **Otherwise**, host them on any public URL you control (your own Pod's public - folder, a static site, GitHub Pages, …) and then update `clientId` **and** the - matching `https://…/redirect.html` entry in `lib/app.dart` to that URL. - -## Verify - -```bash -curl -I https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld # expect 200 -``` - -The `redirect_uris` in the published document must be byte-for-byte identical to -the `redirectUris` list in `lib/app.dart`, including the -`{{orgName}}.{{schemeName}}://redirect` custom scheme. diff --git a/templates/solidpod/solid/client-profile.jsonld.tmpl b/templates/solidpod/solid/client-profile.jsonld.tmpl deleted file mode 100644 index fb40afde..00000000 --- a/templates/solidpod/solid/client-profile.jsonld.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -{ - "@context": "https://www.w3.org/ns/solid/oidc-context.jsonld", - "client_id": "https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld", - "client_name": "{{appName}}", - "application_type": "native", - "redirect_uris": [ - "https://solidcommunity.au/apps/{{projectName}}/redirect.html", - "{{orgName}}.{{schemeName}}://redirect", - "http://localhost:4400/redirect" - ], - "post_logout_redirect_uris": [ - "https://solidcommunity.au/apps/{{projectName}}/redirect.html", - "{{orgName}}.{{schemeName}}://redirect", - "http://localhost:4400/redirect" - ], - "scope": "openid profile offline_access webid", - "grant_types": [ - "authorization_code", - "refresh_token" - ], - "response_types": [ - "code" - ], - "token_endpoint_auth_method": "none" -} diff --git a/templates/solidpod/solid/redirect.html b/templates/solidpod/solid/redirect.html deleted file mode 100644 index 3d64b434..00000000 --- a/templates/solidpod/solid/redirect.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - Flutter Oidc Redirect - - - - - -

Authentication completed! Please close this page.

- - -