Taproom: a Flutter desktop frontend for Homebrew
Browse installed packages, upgrade outdated ones, search formulae and casks, and explore taps, all by driving the brew CLI. Includes a Taskfile for running, testing and building. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# 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
|
||||
|
||||
# Widget Preview related
|
||||
.widget_preview/
|
||||
|
||||
# go-task checksum cache
|
||||
.task/
|
||||
30
.metadata
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 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: "abaf9c523780a608bd46686fd5e53740a07077f8"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: abaf9c523780a608bd46686fd5e53740a07077f8
|
||||
base_revision: abaf9c523780a608bd46686fd5e53740a07077f8
|
||||
- platform: macos
|
||||
create_revision: abaf9c523780a608bd46686fd5e53740a07077f8
|
||||
base_revision: abaf9c523780a608bd46686fd5e53740a07077f8
|
||||
|
||||
# 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'
|
||||
22
README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Taproom
|
||||
|
||||
A Flutter desktop frontend for Homebrew: browse what's installed, see and
|
||||
apply upgrades, search formulae and casks, and explore your taps.
|
||||
|
||||
It drives the `brew` CLI directly (JSON output for queries, streamed output
|
||||
for installs), so it needs Homebrew at `/opt/homebrew` or `/usr/local` and
|
||||
runs **without the macOS App Sandbox** (see `macos/Runner/*.entitlements`),
|
||||
since a sandboxed app can't launch `brew`.
|
||||
|
||||
Casks whose installers need `sudo` prompt for your password via a native
|
||||
dialog (a `SUDO_ASKPASS` helper written to the temp dir at launch).
|
||||
|
||||
## Develop
|
||||
|
||||
Tasks are defined in `Taskfile.yml` ([go-task](https://taskfile.dev),
|
||||
`brew install go-task`); run `task` to list them.
|
||||
|
||||
task run # debug build; needs full Xcode, not just the Command Line Tools
|
||||
task check # format check + analyze + tests (tests include read-only
|
||||
# checks against your real brew)
|
||||
task install # release build copied to /Applications
|
||||
82
Taskfile.yml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# https://taskfile.dev — run `task` to list tasks.
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
APP: build/macos/Build/Products/Release/taproom.app
|
||||
|
||||
tasks:
|
||||
default:
|
||||
silent: true
|
||||
cmds:
|
||||
- task --list
|
||||
|
||||
deps:
|
||||
desc: Fetch Dart/Flutter packages
|
||||
sources: [pubspec.yaml, pubspec.lock]
|
||||
cmds:
|
||||
- flutter pub get
|
||||
|
||||
run:
|
||||
desc: Run the app in debug mode (needs full Xcode)
|
||||
deps: [deps]
|
||||
cmds:
|
||||
- flutter run -d macos
|
||||
|
||||
test:
|
||||
desc: Run all tests (includes read-only checks against your real brew)
|
||||
deps: [deps]
|
||||
cmds:
|
||||
- flutter test {{.CLI_ARGS}}
|
||||
|
||||
analyze:
|
||||
desc: Static analysis
|
||||
deps: [deps]
|
||||
cmds:
|
||||
- flutter analyze
|
||||
|
||||
fmt:
|
||||
desc: Format all Dart code
|
||||
cmds:
|
||||
- dart format lib test
|
||||
|
||||
fmt:check:
|
||||
desc: Fail if any Dart code is unformatted
|
||||
cmds:
|
||||
- dart format --output=none --set-exit-if-changed lib test
|
||||
|
||||
check:
|
||||
desc: Formatting, analysis and tests — run before committing
|
||||
cmds:
|
||||
- task: fmt:check
|
||||
- task: analyze
|
||||
- task: test
|
||||
|
||||
build:
|
||||
desc: Build the release .app (needs full Xcode)
|
||||
deps: [deps]
|
||||
cmds:
|
||||
- flutter build macos --release
|
||||
- echo "Built {{.APP}}"
|
||||
|
||||
open:
|
||||
desc: Launch the release build
|
||||
deps: [build]
|
||||
cmds:
|
||||
- open "{{.APP}}"
|
||||
|
||||
install:
|
||||
desc: Copy the release build to /Applications
|
||||
deps: [build]
|
||||
cmds:
|
||||
- rm -rf /Applications/Taproom.app
|
||||
- cp -R "{{.APP}}" /Applications/Taproom.app
|
||||
|
||||
doctor:
|
||||
desc: Check the Flutter/Xcode toolchain
|
||||
cmds:
|
||||
- flutter doctor
|
||||
|
||||
clean:
|
||||
desc: Remove build outputs
|
||||
cmds:
|
||||
- flutter clean
|
||||
33
analysis_options.yaml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# 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
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- build/**
|
||||
- macos/**
|
||||
|
||||
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
|
||||
240
lib/brew/brew_client.dart
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'models.dart';
|
||||
|
||||
typedef ProcessRunner = Future<ProcessResult> Function(
|
||||
String executable,
|
||||
List<String> args,
|
||||
Map<String, String> environment,
|
||||
);
|
||||
|
||||
typedef ProcessStarter = Future<Process> Function(
|
||||
String executable,
|
||||
List<String> args,
|
||||
Map<String, String> environment,
|
||||
);
|
||||
|
||||
class BrewException implements Exception {
|
||||
BrewException(this.args, this.exitCode, this.stderr);
|
||||
|
||||
final List<String> args;
|
||||
final int exitCode;
|
||||
final String stderr;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'brew ${args.join(' ')} exited $exitCode${stderr.isEmpty ? '' : ':\n$stderr'}';
|
||||
}
|
||||
|
||||
/// Thin wrapper over the `brew` CLI. Queries go through [runner] and are
|
||||
/// parsed from brew's JSON output; mutations (install, tap, ...) are
|
||||
/// returned as [BrewTask]s that stream their output.
|
||||
class BrewClient {
|
||||
BrewClient({
|
||||
required this.brewPath,
|
||||
ProcessRunner? runner,
|
||||
ProcessStarter? starter,
|
||||
this.askpassPath,
|
||||
}) : _runner = runner ?? _defaultRunner,
|
||||
_starter = starter ?? _defaultStarter;
|
||||
|
||||
final String brewPath;
|
||||
|
||||
/// A SUDO_ASKPASS helper, so casks that run `sudo` (pkg installers) can
|
||||
/// prompt for a password without a terminal.
|
||||
final String? askpassPath;
|
||||
final ProcessRunner _runner;
|
||||
final ProcessStarter _starter;
|
||||
|
||||
static const _candidatePaths = [
|
||||
'/opt/homebrew/bin/brew', // Apple Silicon
|
||||
'/usr/local/bin/brew', // Intel
|
||||
'/home/linuxbrew/.linuxbrew/bin/brew',
|
||||
];
|
||||
|
||||
/// GUI apps don't inherit the shell's PATH, so look in brew's standard
|
||||
/// install locations rather than relying on `which`.
|
||||
static String? locate() {
|
||||
for (final p in _candidatePaths) {
|
||||
if (File(p).existsSync()) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<ProcessResult> _defaultRunner(
|
||||
String exe,
|
||||
List<String> args,
|
||||
Map<String, String> env,
|
||||
) => Process.run(exe, args, environment: env);
|
||||
|
||||
static Future<Process> _defaultStarter(
|
||||
String exe,
|
||||
List<String> args,
|
||||
Map<String, String> env,
|
||||
) => Process.start(exe, args, environment: env);
|
||||
|
||||
Map<String, String> _environment({required bool readOnly}) {
|
||||
final inherited = Platform.environment;
|
||||
final brewBin = File(brewPath).parent.path;
|
||||
return {
|
||||
...inherited,
|
||||
'PATH':
|
||||
'$brewBin:/usr/bin:/bin:/usr/sbin:/sbin'
|
||||
'${inherited['PATH'] == null ? '' : ':${inherited['PATH']}'}',
|
||||
'HOMEBREW_NO_COLOR': '1',
|
||||
'HOMEBREW_NO_EMOJI': '1',
|
||||
'HOMEBREW_NO_ENV_HINTS': '1',
|
||||
'NONINTERACTIVE': '1',
|
||||
// Keep queries snappy; an explicit "brew update" is one click away.
|
||||
if (readOnly) 'HOMEBREW_NO_AUTO_UPDATE': '1',
|
||||
'SUDO_ASKPASS': ?askpassPath,
|
||||
};
|
||||
}
|
||||
|
||||
Future<String> _run(List<String> args, {bool allowFailure = false}) async {
|
||||
final r = await _runner(brewPath, args, _environment(readOnly: true));
|
||||
if (r.exitCode != 0 && !allowFailure) {
|
||||
throw BrewException(args, r.exitCode, (r.stderr as String).trim());
|
||||
}
|
||||
return r.stdout as String;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _runJson(List<String> args) async =>
|
||||
jsonDecode(await _run(args)) as Map<String, dynamic>;
|
||||
|
||||
Future<List<Package>> installed() async => Package.listFromInfoJson(
|
||||
await _runJson(['info', '--json=v2', '--installed']),
|
||||
);
|
||||
|
||||
Future<List<OutdatedEntry>> outdated({bool greedy = false}) async =>
|
||||
OutdatedEntry.listFromJson(
|
||||
await _runJson(['outdated', '--json=v2', if (greedy) '--greedy']),
|
||||
);
|
||||
|
||||
Future<List<Tap>> taps() async {
|
||||
final out = await _run(['tap-info', '--json', '--installed']);
|
||||
return (jsonDecode(out) as List)
|
||||
.map((t) => Tap.fromJson(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<Package?> info(String name, PackageKind kind) async {
|
||||
final list = Package.listFromInfoJson(
|
||||
await _runJson(['info', '--json=v2', kind.flag, name]),
|
||||
);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
/// Searches formulae and casks by name. Like `brew search`, a query
|
||||
/// wrapped in slashes is treated as a regex.
|
||||
Future<List<SearchResult>> search(String query) async {
|
||||
final results = await Future.wait([
|
||||
for (final kind in PackageKind.values)
|
||||
// brew exits non-zero when nothing matches; that's just no results.
|
||||
_run([
|
||||
'search',
|
||||
kind.flag,
|
||||
query,
|
||||
], allowFailure: true).then((out) => parseSearchOutput(out, kind)),
|
||||
]);
|
||||
return results.expand((r) => r).toList();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static List<SearchResult> parseSearchOutput(String out, PackageKind kind) => [
|
||||
for (final line in const LineSplitter().convert(out))
|
||||
if (line.trim().isNotEmpty &&
|
||||
!line.startsWith('==>') &&
|
||||
!line.contains(' '))
|
||||
SearchResult(line.trim(), kind),
|
||||
];
|
||||
|
||||
BrewTask install(String name, PackageKind kind) =>
|
||||
task('Install $name', ['install', kind.flag, name]);
|
||||
|
||||
BrewTask uninstall(String name, PackageKind kind) =>
|
||||
task('Uninstall $name', ['uninstall', kind.flag, name]);
|
||||
|
||||
BrewTask reinstall(String name, PackageKind kind) =>
|
||||
task('Reinstall $name', ['reinstall', kind.flag, name]);
|
||||
|
||||
/// Upgrades one package, or everything outdated when [name] is null.
|
||||
BrewTask upgrade({String? name, PackageKind? kind, bool greedy = false}) =>
|
||||
task(name == null ? 'Upgrade all' : 'Upgrade $name', [
|
||||
'upgrade',
|
||||
if (kind != null) kind.flag,
|
||||
if (greedy) '--greedy',
|
||||
?name,
|
||||
]);
|
||||
|
||||
BrewTask pin(String name, {required bool pinned}) => task(
|
||||
'${pinned ? 'Pin' : 'Unpin'} $name',
|
||||
[pinned ? 'pin' : 'unpin', name],
|
||||
);
|
||||
|
||||
BrewTask update() => task('Update Homebrew', ['update']);
|
||||
|
||||
BrewTask cleanup() => task('Clean up', ['cleanup']);
|
||||
|
||||
BrewTask addTap(String name, {String? url}) =>
|
||||
task('Tap $name', ['tap', name, if (url != null && url.isNotEmpty) url]);
|
||||
|
||||
BrewTask removeTap(String name) => task('Untap $name', ['untap', name]);
|
||||
|
||||
BrewTask task(String title, List<String> args) => BrewTask._(
|
||||
title: title,
|
||||
args: args,
|
||||
start: () => _starter(brewPath, args, _environment(readOnly: false)),
|
||||
);
|
||||
}
|
||||
|
||||
/// A running (or finished) brew command whose output is streamed into
|
||||
/// [lines]. Listen to it to rebuild as output arrives.
|
||||
class BrewTask extends ChangeNotifier {
|
||||
BrewTask._({required this.title, required this.args, required this.start});
|
||||
|
||||
final String title;
|
||||
final List<String> args;
|
||||
final Future<Process> Function() start;
|
||||
|
||||
final List<String> lines = [];
|
||||
int? exitCode;
|
||||
Process? _process;
|
||||
bool _started = false;
|
||||
|
||||
bool get running => _started && exitCode == null;
|
||||
bool get succeeded => exitCode == 0;
|
||||
String get command => 'brew ${args.join(' ')}';
|
||||
|
||||
/// Starts the command; completes with its exit code.
|
||||
Future<int> run() async {
|
||||
if (_started) throw StateError('BrewTask already started');
|
||||
_started = true;
|
||||
lines.add('\$ $command');
|
||||
notifyListeners();
|
||||
try {
|
||||
final p = _process = await start();
|
||||
await p.stdin.close();
|
||||
Future<void> pump(Stream<List<int>> s) => s
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.forEach((l) {
|
||||
lines.add(l);
|
||||
notifyListeners();
|
||||
});
|
||||
await Future.wait([pump(p.stdout), pump(p.stderr)]);
|
||||
exitCode = await p.exitCode;
|
||||
} catch (e) {
|
||||
lines.add('Failed to run brew: $e');
|
||||
exitCode = -1;
|
||||
}
|
||||
notifyListeners();
|
||||
return exitCode!;
|
||||
}
|
||||
|
||||
void cancel() => _process?.kill();
|
||||
}
|
||||
195
lib/brew/models.dart
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
enum PackageKind {
|
||||
formula,
|
||||
cask;
|
||||
|
||||
/// The flag brew uses to disambiguate a name, e.g. `brew info --cask foo`.
|
||||
String get flag => this == formula ? '--formula' : '--cask';
|
||||
|
||||
String get label => this == formula ? 'Formula' : 'Cask';
|
||||
}
|
||||
|
||||
/// A formula or cask, as described by `brew info --json=v2`.
|
||||
class Package {
|
||||
const Package({
|
||||
required this.name,
|
||||
required this.fullName,
|
||||
required this.kind,
|
||||
this.displayName,
|
||||
this.tap,
|
||||
this.desc,
|
||||
this.homepage,
|
||||
this.latestVersion,
|
||||
this.installedVersion,
|
||||
this.outdated = false,
|
||||
this.pinned = false,
|
||||
this.installedOnRequest = false,
|
||||
this.dependencies = const [],
|
||||
this.caveats,
|
||||
this.deprecated = false,
|
||||
});
|
||||
|
||||
/// Formula name or cask token; what you pass to `brew install`.
|
||||
final String name;
|
||||
final String fullName;
|
||||
final PackageKind kind;
|
||||
|
||||
/// Casks have a human name ("Firefox") separate from their token.
|
||||
final String? displayName;
|
||||
final String? tap;
|
||||
final String? desc;
|
||||
final String? homepage;
|
||||
final String? latestVersion;
|
||||
final String? installedVersion;
|
||||
final bool outdated;
|
||||
final bool pinned;
|
||||
|
||||
/// False for formulae only present as a dependency of something else.
|
||||
final bool installedOnRequest;
|
||||
final List<String> dependencies;
|
||||
final String? caveats;
|
||||
final bool deprecated;
|
||||
|
||||
bool get isInstalled => installedVersion != null;
|
||||
|
||||
factory Package.fromFormulaJson(Map<String, dynamic> j) {
|
||||
final installs = (j['installed'] as List? ?? const [])
|
||||
.cast<Map<String, dynamic>>();
|
||||
return Package(
|
||||
name: j['name'] as String,
|
||||
fullName: (j['full_name'] as String?) ?? j['name'] as String,
|
||||
kind: PackageKind.formula,
|
||||
tap: j['tap'] as String?,
|
||||
desc: j['desc'] as String?,
|
||||
homepage: j['homepage'] as String?,
|
||||
latestVersion: (j['versions'] as Map?)?['stable'] as String?,
|
||||
installedVersion: installs.isEmpty
|
||||
? null
|
||||
: installs.last['version'] as String?,
|
||||
outdated: j['outdated'] == true,
|
||||
pinned: j['pinned'] == true,
|
||||
installedOnRequest: installs.any(
|
||||
(i) => i['installed_on_request'] == true,
|
||||
),
|
||||
dependencies: (j['dependencies'] as List? ?? const []).cast<String>(),
|
||||
caveats: j['caveats'] as String?,
|
||||
deprecated: j['deprecated'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
factory Package.fromCaskJson(Map<String, dynamic> j) {
|
||||
final names = (j['name'] as List? ?? const []).cast<String>();
|
||||
final installed = j['installed'] as String?;
|
||||
return Package(
|
||||
name: j['token'] as String,
|
||||
fullName: (j['full_token'] as String?) ?? j['token'] as String,
|
||||
kind: PackageKind.cask,
|
||||
displayName: names.isEmpty ? null : names.first,
|
||||
tap: j['tap'] as String?,
|
||||
desc: j['desc'] as String?,
|
||||
homepage: j['homepage'] as String?,
|
||||
latestVersion: j['version'] as String?,
|
||||
installedVersion: installed,
|
||||
outdated: j['outdated'] == true,
|
||||
pinned: j['pinned'] == true,
|
||||
// Casks have no dependency-only installs.
|
||||
installedOnRequest: installed != null,
|
||||
caveats: j['caveats'] as String?,
|
||||
deprecated: j['deprecated'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses the `{"formulae": [...], "casks": [...]}` shape of
|
||||
/// `brew info --json=v2`.
|
||||
static List<Package> listFromInfoJson(Map<String, dynamic> j) => [
|
||||
for (final f in (j['formulae'] as List? ?? const []))
|
||||
Package.fromFormulaJson(f as Map<String, dynamic>),
|
||||
for (final c in (j['casks'] as List? ?? const []))
|
||||
Package.fromCaskJson(c as Map<String, dynamic>),
|
||||
];
|
||||
}
|
||||
|
||||
/// An entry from `brew tap-info --json`.
|
||||
class Tap {
|
||||
const Tap({
|
||||
required this.name,
|
||||
this.path,
|
||||
this.remote,
|
||||
this.official = false,
|
||||
this.cloned = false,
|
||||
this.branch,
|
||||
this.lastCommit,
|
||||
this.formulaNames = const [],
|
||||
this.caskTokens = const [],
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String? path;
|
||||
final String? remote;
|
||||
final bool official;
|
||||
|
||||
/// Whether the tap is a local git checkout. homebrew/core and
|
||||
/// homebrew/cask are usually served from the JSON API instead, so this is
|
||||
/// false for them even though they're always available.
|
||||
final bool cloned;
|
||||
|
||||
/// Git details; only present for cloned taps.
|
||||
final String? branch;
|
||||
final String? lastCommit;
|
||||
final List<String> formulaNames;
|
||||
final List<String> caskTokens;
|
||||
|
||||
factory Tap.fromJson(Map<String, dynamic> j) => Tap(
|
||||
name: j['name'] as String,
|
||||
path: j['path'] as String?,
|
||||
// `remote` is the URL; `custom_remote` is just a flag for whether
|
||||
// it was overridden.
|
||||
remote: j['remote'] as String?,
|
||||
official: j['official'] == true,
|
||||
cloned: j['installed'] == true,
|
||||
branch: j['branch'] as String?,
|
||||
lastCommit: j['last_commit'] as String?,
|
||||
formulaNames: (j['formula_names'] as List? ?? const []).cast<String>(),
|
||||
caskTokens: (j['cask_tokens'] as List? ?? const []).cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
/// An entry from `brew outdated --json=v2`.
|
||||
class OutdatedEntry {
|
||||
const OutdatedEntry({
|
||||
required this.name,
|
||||
required this.kind,
|
||||
required this.installedVersions,
|
||||
required this.currentVersion,
|
||||
this.pinned = false,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final PackageKind kind;
|
||||
final List<String> installedVersions;
|
||||
final String currentVersion;
|
||||
final bool pinned;
|
||||
|
||||
factory OutdatedEntry.fromJson(Map<String, dynamic> j, PackageKind kind) =>
|
||||
OutdatedEntry(
|
||||
name: j['name'] as String,
|
||||
kind: kind,
|
||||
installedVersions: (j['installed_versions'] as List? ?? const [])
|
||||
.cast<String>(),
|
||||
currentVersion: j['current_version'] as String? ?? '?',
|
||||
pinned: j['pinned'] == true,
|
||||
);
|
||||
|
||||
static List<OutdatedEntry> listFromJson(Map<String, dynamic> j) => [
|
||||
for (final f in (j['formulae'] as List? ?? const []))
|
||||
OutdatedEntry.fromJson(f as Map<String, dynamic>, PackageKind.formula),
|
||||
for (final c in (j['casks'] as List? ?? const []))
|
||||
OutdatedEntry.fromJson(c as Map<String, dynamic>, PackageKind.cask),
|
||||
];
|
||||
}
|
||||
|
||||
class SearchResult {
|
||||
const SearchResult(this.name, this.kind);
|
||||
|
||||
final String name;
|
||||
final PackageKind kind;
|
||||
}
|
||||
184
lib/main.dart
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'brew/brew_client.dart';
|
||||
import 'state/app_state.dart';
|
||||
import 'ui/common.dart';
|
||||
import 'ui/installed_page.dart';
|
||||
import 'ui/outdated_page.dart';
|
||||
import 'ui/search_page.dart';
|
||||
import 'ui/taps_page.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
AppState? state;
|
||||
final brewPath = BrewClient.locate();
|
||||
if (brewPath != null) {
|
||||
state = AppState(
|
||||
BrewClient(brewPath: brewPath, askpassPath: _writeAskpass()),
|
||||
);
|
||||
state.refresh();
|
||||
}
|
||||
runApp(TaproomApp(state: state));
|
||||
}
|
||||
|
||||
/// Writes a SUDO_ASKPASS helper that asks for the password with a native
|
||||
/// dialog, for casks whose installers need sudo. Returns null if it can't.
|
||||
String? _writeAskpass() {
|
||||
try {
|
||||
final f = File('${Directory.systemTemp.path}/taproom-askpass.sh');
|
||||
f.writeAsStringSync('''#!/bin/sh
|
||||
exec /usr/bin/osascript -e 'text returned of (display dialog "Homebrew needs your password to continue." default answer "" with hidden answer with title "Taproom" with icon caution)'
|
||||
''');
|
||||
Process.runSync('chmod', ['700', f.path]);
|
||||
return f.path;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TaproomApp extends StatelessWidget {
|
||||
const TaproomApp({super.key, required this.state});
|
||||
|
||||
/// Null when Homebrew isn't installed.
|
||||
final AppState? state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData theme(Brightness b) => ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFE0A030),
|
||||
brightness: b,
|
||||
),
|
||||
);
|
||||
final app = MaterialApp(
|
||||
title: 'Taproom',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: theme(Brightness.light),
|
||||
darkTheme: theme(Brightness.dark),
|
||||
home: state == null ? const _NoBrew() : const HomeScreen(),
|
||||
);
|
||||
// Above MaterialApp so dialogs (which live in the root overlay) see it.
|
||||
return state == null ? app : AppScope(state: state!, child: app);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _index = 0;
|
||||
|
||||
static const _pages = [
|
||||
InstalledPage(),
|
||||
OutdatedPage(),
|
||||
SearchPage(),
|
||||
TapsPage(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
final outdated = state.outdated.length;
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
selectedIndex: _index,
|
||||
onDestinationSelected: (i) => setState(() => _index = i),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Icon(Icons.sports_bar, size: 32),
|
||||
),
|
||||
destinations: [
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
selectedIcon: Icon(Icons.inventory_2),
|
||||
label: Text('Installed'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Badge(
|
||||
isLabelVisible: outdated > 0,
|
||||
label: Text('$outdated'),
|
||||
child: const Icon(Icons.update),
|
||||
),
|
||||
label: const Text('Outdated'),
|
||||
),
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.search),
|
||||
label: Text('Search'),
|
||||
),
|
||||
const NavigationRailDestination(
|
||||
icon: Icon(Icons.water_drop_outlined),
|
||||
selectedIcon: Icon(Icons.water_drop),
|
||||
label: Text('Taps'),
|
||||
),
|
||||
],
|
||||
trailing: Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'brew update — fetch the latest package index',
|
||||
icon: const Icon(Icons.cloud_sync_outlined),
|
||||
onPressed: () => runTask(context, state.brew.update()),
|
||||
),
|
||||
IconButton(
|
||||
tooltip:
|
||||
'brew cleanup — remove old versions and caches',
|
||||
icon: const Icon(Icons.cleaning_services_outlined),
|
||||
onPressed: () => runTask(context, state.brew.cleanup()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: IndexedStack(index: _index, children: _pages),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoBrew extends StatelessWidget {
|
||||
const _NoBrew();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.sports_bar, size: 64),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Homebrew not found',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Looked in /opt/homebrew and /usr/local.'),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => openUrl('https://brew.sh'),
|
||||
child: const Text('Get Homebrew'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
54
lib/state/app_state.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../brew/brew_client.dart';
|
||||
import '../brew/models.dart';
|
||||
|
||||
/// Everything the app knows about the local Homebrew install. Pages read
|
||||
/// from here; anything that changes the install calls [refresh] afterwards.
|
||||
class AppState extends ChangeNotifier {
|
||||
AppState(this.brew);
|
||||
|
||||
final BrewClient brew;
|
||||
|
||||
List<Package> installed = const [];
|
||||
List<OutdatedEntry> outdated = const [];
|
||||
List<Tap> taps = const [];
|
||||
|
||||
/// Also report casks that update themselves or have version :latest.
|
||||
bool greedy = false;
|
||||
bool loading = false;
|
||||
Object? error;
|
||||
|
||||
Future<void> refresh() async {
|
||||
loading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
brew.installed(),
|
||||
brew.outdated(greedy: greedy),
|
||||
brew.taps(),
|
||||
]);
|
||||
installed = results[0] as List<Package>;
|
||||
outdated = results[1] as List<OutdatedEntry>;
|
||||
taps = results[2] as List<Tap>;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setGreedy(bool value) async {
|
||||
greedy = value;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Package? installedPackage(String name, PackageKind kind) {
|
||||
for (final p in installed) {
|
||||
if (p.kind == kind && (p.name == name || p.fullName == name)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
204
lib/ui/common.dart
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../brew/brew_client.dart';
|
||||
import '../brew/models.dart';
|
||||
import '../state/app_state.dart';
|
||||
import 'task_dialog.dart';
|
||||
|
||||
/// Makes the [AppState] available to the widget tree.
|
||||
class AppScope extends InheritedNotifier<AppState> {
|
||||
const AppScope({super.key, required AppState state, required super.child})
|
||||
: super(notifier: state);
|
||||
|
||||
/// Reads the state and rebuilds the caller when it changes.
|
||||
static AppState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<AppScope>()!.notifier!;
|
||||
|
||||
/// Reads the state without subscribing, for use in callbacks.
|
||||
static AppState read(BuildContext context) =>
|
||||
context.getInheritedWidgetOfExactType<AppScope>()!.notifier!;
|
||||
}
|
||||
|
||||
/// Runs [task] in a dialog showing its live output, then refreshes the app
|
||||
/// state. Returns whether the command succeeded.
|
||||
Future<bool> runTask(BuildContext context, BrewTask task) async {
|
||||
final state = AppScope.read(context);
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => TaskDialog(task: task),
|
||||
);
|
||||
// Not awaited: the lists update in place once brew answers.
|
||||
state.refresh();
|
||||
return task.succeeded;
|
||||
}
|
||||
|
||||
Future<bool> confirm(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
String action = 'OK',
|
||||
}) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(action),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return ok ?? false;
|
||||
}
|
||||
|
||||
void openUrl(String url) => Process.run('open', [url]);
|
||||
|
||||
IconData kindIcon(PackageKind kind) =>
|
||||
kind == PackageKind.formula ? Icons.terminal : Icons.apps;
|
||||
|
||||
/// Title row + optional controls shown at the top of every page.
|
||||
class PageHeader extends StatelessWidget {
|
||||
const PageHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions = const [],
|
||||
this.bottom,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
final Widget? bottom;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(width: 12),
|
||||
if (state.loading)
|
||||
const SizedBox.square(
|
||||
dimension: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const Spacer(),
|
||||
...actions,
|
||||
IconButton(
|
||||
tooltip: 'Reload',
|
||||
onPressed: state.loading ? null : state.refresh,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (state.error != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
ErrorBanner(state.error!),
|
||||
],
|
||||
if (bottom != null) ...[const SizedBox(height: 12), bottom!],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorBanner extends StatelessWidget {
|
||||
const ErrorBanner(this.error, {super.key});
|
||||
|
||||
final Object error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(
|
||||
'$error',
|
||||
style: TextStyle(color: scheme.onErrorContainer),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tag extends StatelessWidget {
|
||||
const Tag(this.label, {super.key, this.color});
|
||||
|
||||
final String label;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = color ?? Theme.of(context).colorScheme.secondary;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: c),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(color: c),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyState extends StatelessWidget {
|
||||
const EmptyState(this.message, {super.key, this.icon = Icons.inbox});
|
||||
|
||||
final String message;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.outline;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: color),
|
||||
const SizedBox(height: 12),
|
||||
Text(message, style: TextStyle(color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A search box used to filter lists.
|
||||
class FilterField extends StatelessWidget {
|
||||
const FilterField({super.key, required this.onChanged, this.hint = 'Filter'});
|
||||
|
||||
final ValueChanged<String> onChanged;
|
||||
final String hint;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextField(
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.filter_list),
|
||||
hintText: hint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
135
lib/ui/installed_page.dart
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../brew/models.dart';
|
||||
import 'common.dart';
|
||||
import 'package_details.dart';
|
||||
|
||||
class InstalledPage extends StatefulWidget {
|
||||
const InstalledPage({super.key});
|
||||
|
||||
@override
|
||||
State<InstalledPage> createState() => _InstalledPageState();
|
||||
}
|
||||
|
||||
class _InstalledPageState extends State<InstalledPage> {
|
||||
String _filter = '';
|
||||
PackageKind? _kind;
|
||||
|
||||
/// Hide formulae that were only pulled in as dependencies.
|
||||
bool _requestedOnly = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
final q = _filter.toLowerCase();
|
||||
final packages =
|
||||
state.installed
|
||||
.where((p) => _kind == null || p.kind == _kind)
|
||||
.where((p) => !_requestedOnly || p.installedOnRequest)
|
||||
.where(
|
||||
(p) =>
|
||||
q.isEmpty ||
|
||||
p.name.toLowerCase().contains(q) ||
|
||||
(p.displayName?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.desc?.toLowerCase().contains(q) ?? false),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'Installed',
|
||||
bottom: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilterField(
|
||||
onChanged: (v) => setState(() => _filter = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SegmentedButton<PackageKind?>(
|
||||
segments: const [
|
||||
ButtonSegment(value: null, label: Text('All')),
|
||||
ButtonSegment(
|
||||
value: PackageKind.formula,
|
||||
label: Text('Formulae'),
|
||||
),
|
||||
ButtonSegment(value: PackageKind.cask, label: Text('Casks')),
|
||||
],
|
||||
selected: {_kind},
|
||||
onSelectionChanged: (s) => setState(() => _kind = s.first),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FilterChip(
|
||||
label: const Text('Requested only'),
|
||||
tooltip: 'Hide formulae installed only as dependencies',
|
||||
selected: _requestedOnly,
|
||||
onSelected: (v) => setState(() => _requestedOnly = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
'${packages.length} of ${state.installed.length}',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: packages.isEmpty
|
||||
? EmptyState(
|
||||
state.loading ? 'Loading…' : 'Nothing installed matches.',
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: packages.length,
|
||||
itemBuilder: (context, i) =>
|
||||
PackageTile(package: packages[i]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PackageTile extends StatelessWidget {
|
||||
const PackageTile({super.key, required this.package});
|
||||
|
||||
final Package package;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = package;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return ListTile(
|
||||
leading: Icon(kindIcon(p.kind)),
|
||||
title: Row(
|
||||
children: [
|
||||
Flexible(child: Text(p.name, overflow: TextOverflow.ellipsis)),
|
||||
if (p.displayName != null && p.displayName != p.name)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
p.displayName!,
|
||||
style: TextStyle(color: scheme.outline),
|
||||
),
|
||||
),
|
||||
if (p.outdated) Tag('outdated', color: scheme.tertiary),
|
||||
if (p.pinned) const Tag('pinned'),
|
||||
if (p.kind == PackageKind.formula && !p.installedOnRequest)
|
||||
Tag('dependency', color: scheme.outline),
|
||||
],
|
||||
),
|
||||
subtitle: p.desc == null
|
||||
? null
|
||||
: Text(p.desc!, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: Text(
|
||||
p.installedVersion ?? '',
|
||||
style: const TextStyle(fontFamily: 'Menlo', fontSize: 12),
|
||||
),
|
||||
onTap: () => showPackageDetails(context, p.fullName, p.kind),
|
||||
);
|
||||
}
|
||||
}
|
||||
104
lib/ui/outdated_page.dart
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'common.dart';
|
||||
import 'package_details.dart';
|
||||
|
||||
class OutdatedPage extends StatelessWidget {
|
||||
const OutdatedPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final upgradable = state.outdated.where((e) => !e.pinned).length;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'Outdated',
|
||||
actions: [
|
||||
Tooltip(
|
||||
message:
|
||||
'Include casks that update themselves or use '
|
||||
'version :latest (brew outdated --greedy)',
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('Greedy'),
|
||||
Switch(
|
||||
value: state.greedy,
|
||||
onChanged: state.loading ? null : state.setGreedy,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.upgrade),
|
||||
label: Text('Upgrade all ($upgradable)'),
|
||||
onPressed: upgradable == 0
|
||||
? null
|
||||
: () => runTask(
|
||||
context,
|
||||
state.brew.upgrade(greedy: state.greedy),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: state.outdated.isEmpty
|
||||
? EmptyState(
|
||||
state.loading ? 'Checking…' : 'Everything is up to date.',
|
||||
icon: Icons.check_circle_outline,
|
||||
)
|
||||
: ListView(
|
||||
children: [
|
||||
for (final e in state.outdated)
|
||||
ListTile(
|
||||
leading: Icon(kindIcon(e.kind)),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(e.name),
|
||||
if (e.pinned) const Tag('pinned'),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${e.installedVersions.join(', ')} → ${e.currentVersion}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Menlo',
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
trailing: OutlinedButton(
|
||||
onPressed: e.pinned
|
||||
? null
|
||||
: () => runTask(
|
||||
context,
|
||||
state.brew.upgrade(
|
||||
name: e.name,
|
||||
kind: e.kind,
|
||||
greedy: state.greedy,
|
||||
),
|
||||
),
|
||||
child: const Text('Upgrade'),
|
||||
),
|
||||
onTap: () =>
|
||||
showPackageDetails(context, e.name, e.kind),
|
||||
),
|
||||
if (state.outdated.any((e) => e.pinned))
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Pinned formulae are skipped by upgrades; unpin them '
|
||||
'from their details to upgrade.',
|
||||
style: TextStyle(color: scheme.outline),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
241
lib/ui/package_details.dart
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../brew/models.dart';
|
||||
import 'common.dart';
|
||||
|
||||
Future<void> showPackageDetails(
|
||||
BuildContext context,
|
||||
String name,
|
||||
PackageKind kind,
|
||||
) => showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => PackageDetailsDialog(name: name, kind: kind),
|
||||
);
|
||||
|
||||
/// Full `brew info` for one package, with the actions that apply to it.
|
||||
class PackageDetailsDialog extends StatefulWidget {
|
||||
const PackageDetailsDialog({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.kind,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final PackageKind kind;
|
||||
|
||||
@override
|
||||
State<PackageDetailsDialog> createState() => _PackageDetailsDialogState();
|
||||
}
|
||||
|
||||
class _PackageDetailsDialogState extends State<PackageDetailsDialog> {
|
||||
late Future<Package?> _info;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
void _load() =>
|
||||
_info = AppScope.read(context).brew.info(widget.name, widget.kind);
|
||||
|
||||
Future<void> _act(Future<bool> Function() action) async {
|
||||
await action();
|
||||
if (mounted) setState(_load);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
content: SizedBox(
|
||||
width: 600,
|
||||
child: FutureBuilder<Package?>(
|
||||
future: _info,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const SizedBox(
|
||||
height: 160,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snap.hasError) return ErrorBanner(snap.error!);
|
||||
final p = snap.data;
|
||||
if (p == null) return Text('No information for ${widget.name}.');
|
||||
return _Details(package: p);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FutureBuilder<Package?>(
|
||||
future: _info,
|
||||
builder: (context, snap) => snap.data == null
|
||||
? const SizedBox.shrink()
|
||||
: Wrap(spacing: 8, children: _actions(snap.data!)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _actions(Package p) {
|
||||
final brew = AppScope.read(context).brew;
|
||||
if (!p.isInstalled) {
|
||||
return [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('Install'),
|
||||
onPressed: () =>
|
||||
_act(() => runTask(context, brew.install(p.fullName, p.kind))),
|
||||
),
|
||||
];
|
||||
}
|
||||
return [
|
||||
if (p.outdated && !p.pinned)
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.upgrade),
|
||||
label: const Text('Upgrade'),
|
||||
onPressed: () => _act(
|
||||
() =>
|
||||
runTask(context, brew.upgrade(name: p.fullName, kind: p.kind)),
|
||||
),
|
||||
),
|
||||
if (p.kind == PackageKind.formula)
|
||||
OutlinedButton.icon(
|
||||
icon: Icon(p.pinned ? Icons.push_pin_outlined : Icons.push_pin),
|
||||
label: Text(p.pinned ? 'Unpin' : 'Pin'),
|
||||
onPressed: () => _act(
|
||||
() => runTask(context, brew.pin(p.fullName, pinned: !p.pinned)),
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () =>
|
||||
_act(() => runTask(context, brew.reinstall(p.fullName, p.kind))),
|
||||
child: const Text('Reinstall'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: const Text('Uninstall'),
|
||||
onPressed: () => _act(() async {
|
||||
final ok = await confirm(
|
||||
context,
|
||||
title: 'Uninstall ${p.name}?',
|
||||
message: p.kind == PackageKind.formula
|
||||
? 'Homebrew will refuse if other installed formulae '
|
||||
'depend on it.'
|
||||
: 'This removes the app and its Homebrew-managed files.',
|
||||
action: 'Uninstall',
|
||||
);
|
||||
if (!ok || !mounted) return false;
|
||||
return runTask(context, brew.uninstall(p.fullName, p.kind));
|
||||
}),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _Details extends StatelessWidget {
|
||||
const _Details({required this.package});
|
||||
|
||||
final Package package;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = package;
|
||||
final theme = Theme.of(context);
|
||||
final muted = TextStyle(color: theme.colorScheme.outline);
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(kindIcon(p.kind)),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
p.displayName ?? p.name,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
Tag(p.kind.label),
|
||||
if (p.isInstalled)
|
||||
Tag('installed', color: theme.colorScheme.primary),
|
||||
if (p.outdated)
|
||||
Tag('outdated', color: theme.colorScheme.tertiary),
|
||||
if (p.pinned) const Tag('pinned'),
|
||||
if (p.deprecated)
|
||||
Tag('deprecated', color: theme.colorScheme.error),
|
||||
],
|
||||
),
|
||||
if (p.displayName != null) Text(p.fullName, style: muted),
|
||||
const SizedBox(height: 12),
|
||||
if (p.desc != null) Text(p.desc!, style: theme.textTheme.bodyLarge),
|
||||
const SizedBox(height: 12),
|
||||
_field('Tap', p.tap ?? '—'),
|
||||
_field('Latest', p.latestVersion ?? '—'),
|
||||
_field('Installed', p.installedVersion ?? 'not installed'),
|
||||
if (p.homepage != null)
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 90, child: Text('Homepage')),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
onTap: () => openUrl(p.homepage!),
|
||||
child: Text(
|
||||
p.homepage!,
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (p.dependencies.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text('Dependencies', style: theme.textTheme.titleSmall),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final d in p.dependencies)
|
||||
ActionChip(
|
||||
label: Text(d),
|
||||
onPressed: () =>
|
||||
showPackageDetails(context, d, PackageKind.formula),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (p.caveats != null && p.caveats!.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text('Caveats', style: theme.textTheme.titleSmall),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
p.caveats!.trim(),
|
||||
style: const TextStyle(fontFamily: 'Menlo', fontSize: 12),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field(String label, String value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 90, child: Text(label)),
|
||||
Flexible(child: SelectableText(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
125
lib/ui/search_page.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../brew/models.dart';
|
||||
import 'common.dart';
|
||||
import 'package_details.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
const SearchPage({super.key});
|
||||
|
||||
@override
|
||||
State<SearchPage> createState() => _SearchPageState();
|
||||
}
|
||||
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
final _controller = TextEditingController();
|
||||
Future<List<SearchResult>>? _results;
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _search() {
|
||||
final q = _controller.text.trim();
|
||||
if (q.isEmpty) return;
|
||||
setState(() {
|
||||
_query = q;
|
||||
_results = AppScope.read(context).brew.search(q);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'Search',
|
||||
bottom: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
onSubmitted: (_) => _search(),
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: Icon(Icons.search),
|
||||
hintText: 'Formula or cask name — /regex/ also works',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FilledButton(onPressed: _search, child: const Text('Search')),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _results == null
|
||||
? const EmptyState(
|
||||
'Search all formulae and casks in your taps.',
|
||||
icon: Icons.search,
|
||||
)
|
||||
: FutureBuilder<List<SearchResult>>(
|
||||
future: _results,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ErrorBanner(snap.error!),
|
||||
);
|
||||
}
|
||||
final results = snap.data!;
|
||||
if (results.isEmpty) {
|
||||
return EmptyState('No matches for "$_query".');
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: results.length,
|
||||
itemBuilder: (context, i) {
|
||||
final r = results[i];
|
||||
final installed = state.installedPackage(
|
||||
r.name,
|
||||
r.kind,
|
||||
);
|
||||
return ListTile(
|
||||
leading: Icon(kindIcon(r.kind)),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(r.name),
|
||||
Tag(r.kind.label),
|
||||
if (installed != null)
|
||||
Tag(
|
||||
'installed',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: installed != null
|
||||
? null
|
||||
: OutlinedButton(
|
||||
onPressed: () => runTask(
|
||||
context,
|
||||
state.brew.install(r.name, r.kind),
|
||||
),
|
||||
child: const Text('Install'),
|
||||
),
|
||||
onTap: () =>
|
||||
showPackageDetails(context, r.name, r.kind),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
295
lib/ui/taps_page.dart
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../brew/models.dart';
|
||||
import 'common.dart';
|
||||
import 'package_details.dart';
|
||||
|
||||
/// Lists taps on the left; the selected tap's formulae and casks on the right.
|
||||
class TapsPage extends StatefulWidget {
|
||||
const TapsPage({super.key});
|
||||
|
||||
@override
|
||||
State<TapsPage> createState() => _TapsPageState();
|
||||
}
|
||||
|
||||
class _TapsPageState extends State<TapsPage> {
|
||||
String? _selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
final taps = state.taps;
|
||||
final selected =
|
||||
taps.where((t) => t.name == _selected).firstOrNull ?? taps.firstOrNull;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'Taps',
|
||||
actions: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add tap'),
|
||||
onPressed: () => _addTap(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: ListView(
|
||||
children: [
|
||||
for (final t in taps)
|
||||
ListTile(
|
||||
selected: t.name == selected?.name,
|
||||
leading: Icon(
|
||||
t.official ? Icons.verified : Icons.source,
|
||||
),
|
||||
title: Text(t.name),
|
||||
subtitle: Text(_counts(t)),
|
||||
onTap: () => setState(() => _selected = t.name),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: selected == null
|
||||
? const EmptyState('No taps.')
|
||||
// Keyed so filter/tab state resets when switching taps.
|
||||
: _TapContents(key: ValueKey(selected.name), tap: selected),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static String _counts(Tap t) => [
|
||||
if (t.formulaNames.isNotEmpty) '${t.formulaNames.length} formulae',
|
||||
if (t.caskTokens.isNotEmpty) '${t.caskTokens.length} casks',
|
||||
if (t.formulaNames.isEmpty && t.caskTokens.isEmpty) 'empty',
|
||||
].join(' · ');
|
||||
|
||||
Future<void> _addTap(BuildContext context) async {
|
||||
final name = TextEditingController();
|
||||
final url = TextEditingController();
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Add tap'),
|
||||
content: SizedBox(
|
||||
width: 420,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: name,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
hintText: 'user/repo',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: url,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Git URL (optional)',
|
||||
hintText: 'Defaults to github.com/user/homebrew-repo',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Tap'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final n = name.text.trim();
|
||||
final u = url.text.trim();
|
||||
name.dispose();
|
||||
url.dispose();
|
||||
if (ok != true || n.isEmpty || !context.mounted) return;
|
||||
await runTask(context, AppScope.read(context).brew.addTap(n, url: u));
|
||||
if (mounted) setState(() => _selected = n);
|
||||
}
|
||||
}
|
||||
|
||||
class _TapContents extends StatefulWidget {
|
||||
const _TapContents({super.key, required this.tap});
|
||||
|
||||
final Tap tap;
|
||||
|
||||
@override
|
||||
State<_TapContents> createState() => _TapContentsState();
|
||||
}
|
||||
|
||||
class _TapContentsState extends State<_TapContents> {
|
||||
String _filter = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = widget.tap;
|
||||
final muted = TextStyle(color: Theme.of(context).colorScheme.outline);
|
||||
final hasBoth = t.formulaNames.isNotEmpty && t.caskTokens.isNotEmpty;
|
||||
final kinds = [
|
||||
// An empty tap still gets one (empty) list.
|
||||
if (t.formulaNames.isNotEmpty || t.caskTokens.isEmpty)
|
||||
PackageKind.formula,
|
||||
if (t.caskTokens.isNotEmpty) PackageKind.cask,
|
||||
];
|
||||
|
||||
return DefaultTabController(
|
||||
length: kinds.length,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
t.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (t.official) const Tag('official'),
|
||||
if (!t.cloned) Tag('via API', color: muted.color),
|
||||
],
|
||||
),
|
||||
if (t.remote != null)
|
||||
SelectableText(t.remote!, style: muted),
|
||||
if (t.path != null && t.cloned)
|
||||
SelectableText(t.path!, style: muted),
|
||||
if (t.branch != null || t.lastCommit != null)
|
||||
Text(
|
||||
[
|
||||
if (t.branch != null) 'branch ${t.branch}',
|
||||
if (t.lastCommit != null)
|
||||
'last commit ${t.lastCommit}',
|
||||
].join(' · '),
|
||||
style: muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.link_off),
|
||||
label: const Text('Untap'),
|
||||
// homebrew/core and homebrew/cask come from the JSON API
|
||||
// unless cloned; there's nothing local to untap.
|
||||
onPressed: !t.cloned
|
||||
? null
|
||||
: () async {
|
||||
final ok = await confirm(
|
||||
context,
|
||||
title: 'Untap ${t.name}?',
|
||||
message:
|
||||
'Homebrew refuses if you have packages '
|
||||
'installed from this tap.',
|
||||
action: 'Untap',
|
||||
);
|
||||
if (ok && context.mounted) {
|
||||
await runTask(
|
||||
context,
|
||||
AppScope.read(context).brew.removeTap(t.name),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: FilterField(onChanged: (v) => setState(() => _filter = v)),
|
||||
),
|
||||
if (hasBoth)
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Formulae (${t.formulaNames.length})'),
|
||||
Tab(text: 'Casks (${t.caskTokens.length})'),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
for (final k in kinds)
|
||||
_NameList(
|
||||
names: k == PackageKind.formula
|
||||
? t.formulaNames
|
||||
: t.caskTokens,
|
||||
kind: k,
|
||||
filter: _filter,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NameList extends StatelessWidget {
|
||||
const _NameList({
|
||||
required this.names,
|
||||
required this.kind,
|
||||
required this.filter,
|
||||
});
|
||||
|
||||
final List<String> names;
|
||||
final PackageKind kind;
|
||||
final String filter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = AppScope.of(context);
|
||||
final q = filter.toLowerCase();
|
||||
final shown = q.isEmpty
|
||||
? names
|
||||
: names.where((n) => n.toLowerCase().contains(q)).toList();
|
||||
if (shown.isEmpty) return const EmptyState('No matches.');
|
||||
return ListView.builder(
|
||||
itemCount: shown.length,
|
||||
itemExtent: 40,
|
||||
itemBuilder: (context, i) {
|
||||
final name = shown[i];
|
||||
final installed = state.installedPackage(name, kind) != null;
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: Icon(kindIcon(kind), size: 18),
|
||||
title: Text(name),
|
||||
trailing: installed
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () => showPackageDetails(context, name, kind),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
106
lib/ui/task_dialog.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../brew/brew_client.dart';
|
||||
|
||||
/// Runs a [BrewTask] and shows its output as it streams in.
|
||||
class TaskDialog extends StatefulWidget {
|
||||
const TaskDialog({super.key, required this.task});
|
||||
|
||||
final BrewTask task;
|
||||
|
||||
@override
|
||||
State<TaskDialog> createState() => _TaskDialogState();
|
||||
}
|
||||
|
||||
class _TaskDialogState extends State<TaskDialog> {
|
||||
final _scroll = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.task.addListener(_onOutput);
|
||||
widget.task.run();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.task.removeListener(_onOutput);
|
||||
_scroll.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onOutput() {
|
||||
if (!mounted) return;
|
||||
// Follow the output, unless the user scrolled up to read something.
|
||||
final follow =
|
||||
!_scroll.hasClients ||
|
||||
_scroll.position.pixels >= _scroll.position.maxScrollExtent - 40;
|
||||
setState(() {});
|
||||
if (follow) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scroll.hasClients) {
|
||||
_scroll.jumpTo(_scroll.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final task = widget.task;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final Widget status = task.running
|
||||
? const SizedBox.square(
|
||||
dimension: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: task.succeeded
|
||||
? Icon(Icons.check_circle, color: scheme.primary)
|
||||
: Icon(Icons.error, color: scheme.error);
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
status,
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(task.title)),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 720,
|
||||
height: 420,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scroll,
|
||||
child: SelectableText(
|
||||
task.lines.join('\n'),
|
||||
style: const TextStyle(fontFamily: 'Menlo', fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (!task.running && !task.succeeded)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text(
|
||||
'Exited with code ${task.exitCode}',
|
||||
style: TextStyle(color: scheme.error),
|
||||
),
|
||||
),
|
||||
if (task.running)
|
||||
TextButton(onPressed: task.cancel, child: const Text('Cancel'))
|
||||
else
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
7
macos/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||
1
macos/Flutter/Flutter-Debug.xcconfig
Normal file
|
|
@ -0,0 +1 @@
|
|||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
1
macos/Flutter/Flutter-Release.xcconfig
Normal file
|
|
@ -0,0 +1 @@
|
|||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
10
macos/Flutter/GeneratedPluginRegistrant.swift
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
}
|
||||
729
macos/Runner.xcodeproj/project.pbxproj
Normal file
|
|
@ -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 = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* taproom.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "taproom.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* taproom.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 /* taproom.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 = "<group>";
|
||||
};
|
||||
/* 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 = com.philippeterson.taproom.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/taproom.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/taproom";
|
||||
};
|
||||
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 = com.philippeterson.taproom.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/taproom.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/taproom";
|
||||
};
|
||||
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 = com.philippeterson.taproom.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/taproom.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/taproom";
|
||||
};
|
||||
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 = 12.0;
|
||||
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 = 12.0;
|
||||
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 = 12.0;
|
||||
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 */;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
117
macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "taproom.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "taproom.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "taproom.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "taproom.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "taproom.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
macos/Runner/AppDelegate.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 101 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
Normal file
|
After Width: | Height: | Size: 520 B |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
343
macos/Runner/Base.lproj/MainMenu.xib
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="EPT-qC-fAb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
<point key="canvasLocation" x="142" y="-258"/>
|
||||
</menu>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
14
macos/Runner/Configs/AppInfo.xcconfig
Normal file
|
|
@ -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 = taproom
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.philippeterson.taproom
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 com.philippeterson. All rights reserved.
|
||||
2
macos/Runner/Configs/Debug.xcconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
||||
2
macos/Runner/Configs/Release.xcconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
||||
13
macos/Runner/Configs/Warnings.xcconfig
Normal file
|
|
@ -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
|
||||
12
macos/Runner/DebugProfile.entitlements
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
32
macos/Runner/Info.plist
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
15
macos/Runner/MainFlutterWindow.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController()
|
||||
let windowFrame = self.frame
|
||||
self.contentViewController = flutterViewController
|
||||
self.setFrame(windowFrame, display: true)
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
||||
8
macos/Runner/Release.entitlements
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
12
macos/RunnerTests/RunnerTests.swift
Normal file
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
213
pubspec.lock
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
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"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
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"
|
||||
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"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.20"
|
||||
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: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.3"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.2"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
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: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.3.0"
|
||||
sdks:
|
||||
dart: ">=3.13.5 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
89
pubspec.yaml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
name: taproom
|
||||
description: "A Flutter frontend for Homebrew"
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.13.5
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
82
test/app_test.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:taproom/brew/brew_client.dart';
|
||||
import 'package:taproom/main.dart';
|
||||
import 'package:taproom/state/app_state.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('shows installed packages and outdated badge', (tester) async {
|
||||
final client = BrewClient(
|
||||
brewPath: '/fake/brew',
|
||||
runner: (exe, args, env) async {
|
||||
final out = switch (args.first) {
|
||||
'info' => {
|
||||
'formulae': [
|
||||
{
|
||||
'name': 'jq',
|
||||
'desc': 'Lightweight and flexible command-line JSON processor',
|
||||
'installed': [
|
||||
{'version': '1.8.1', 'installed_on_request': true},
|
||||
],
|
||||
'outdated': true,
|
||||
},
|
||||
{
|
||||
'name': 'oniguruma',
|
||||
'installed': [
|
||||
{'version': '6.9.10', 'installed_on_request': false},
|
||||
],
|
||||
},
|
||||
],
|
||||
'casks': [
|
||||
{
|
||||
'token': 'firefox',
|
||||
'name': ['Mozilla Firefox'],
|
||||
'installed': '141.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
'outdated' => {
|
||||
'formulae': [
|
||||
{
|
||||
'name': 'jq',
|
||||
'installed_versions': ['1.8.1'],
|
||||
'current_version': '1.8.2',
|
||||
},
|
||||
],
|
||||
'casks': [],
|
||||
},
|
||||
'tap-info' => [
|
||||
{
|
||||
'name': 'homebrew/core',
|
||||
'official': true,
|
||||
'formula_names': ['jq'],
|
||||
},
|
||||
],
|
||||
_ => throw UnimplementedError('$args'),
|
||||
};
|
||||
return ProcessResult(0, 0, jsonEncode(out), '');
|
||||
},
|
||||
);
|
||||
final state = AppState(client);
|
||||
await tester.binding.setSurfaceSize(const Size(1200, 800));
|
||||
await tester.pumpWidget(TaproomApp(state: state));
|
||||
await tester.runAsync(state.refresh);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('jq'), findsOneWidget);
|
||||
expect(find.text('oniguruma'), findsOneWidget);
|
||||
expect(find.text('Mozilla Firefox'), findsOneWidget);
|
||||
expect(find.text('dependency'), findsOneWidget);
|
||||
expect(find.text('3 of 3'), findsOneWidget);
|
||||
// Badge on the Outdated rail item.
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Requested only'));
|
||||
await tester.pump();
|
||||
expect(find.text('oniguruma'), findsNothing);
|
||||
expect(find.text('2 of 3'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
177
test/brew_client_test.dart
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:taproom/brew/brew_client.dart';
|
||||
import 'package:taproom/brew/models.dart';
|
||||
|
||||
/// Records calls and answers each with a canned result chosen by [respond].
|
||||
class FakeRunner {
|
||||
FakeRunner(this.respond);
|
||||
|
||||
final ProcessResult Function(List<String> args) respond;
|
||||
final calls = <(List<String>, Map<String, String>)>[];
|
||||
|
||||
Future<ProcessResult> call(
|
||||
String exe,
|
||||
List<String> args,
|
||||
Map<String, String> env,
|
||||
) async {
|
||||
calls.add((args, env));
|
||||
return respond(args);
|
||||
}
|
||||
}
|
||||
|
||||
ProcessResult result(String stdout, {int exitCode = 0, String stderr = ''}) =>
|
||||
ProcessResult(0, exitCode, stdout, stderr);
|
||||
|
||||
void main() {
|
||||
test('search queries formulae and casks and tolerates no matches', () async {
|
||||
final runner = FakeRunner(
|
||||
(args) => args.contains('--formula')
|
||||
? result('jq\njq-lsp\n')
|
||||
// brew exits 1 with a message when nothing matches.
|
||||
: result('', exitCode: 1, stderr: 'Error: No casks found for "jq".'),
|
||||
);
|
||||
final brew = BrewClient(
|
||||
brewPath: '/opt/homebrew/bin/brew',
|
||||
runner: runner.call,
|
||||
);
|
||||
|
||||
final results = await brew.search('jq');
|
||||
|
||||
expect(results.map((r) => (r.name, r.kind)), [
|
||||
('jq', PackageKind.formula),
|
||||
('jq-lsp', PackageKind.formula),
|
||||
]);
|
||||
expect(runner.calls.map((c) => c.$1), [
|
||||
['search', '--formula', 'jq'],
|
||||
['search', '--cask', 'jq'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('search output parsing skips headers and prose', () {
|
||||
final r = BrewClient.parseSearchOutput(
|
||||
'==> Formulae\nfoo\n\nIf you meant "foo" specifically:\nbar\n',
|
||||
PackageKind.formula,
|
||||
);
|
||||
expect(r.map((e) => e.name), ['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('queries run with a usable PATH and without auto-update', () async {
|
||||
final runner = FakeRunner((_) => result('{"formulae": [], "casks": []}'));
|
||||
final brew = BrewClient(
|
||||
brewPath: '/opt/homebrew/bin/brew',
|
||||
runner: runner.call,
|
||||
);
|
||||
|
||||
await brew.installed();
|
||||
|
||||
final env = runner.calls.single.$2;
|
||||
expect(env['PATH'], startsWith('/opt/homebrew/bin:/usr/bin:'));
|
||||
expect(env['HOMEBREW_NO_AUTO_UPDATE'], '1');
|
||||
expect(env['HOMEBREW_NO_COLOR'], '1');
|
||||
});
|
||||
|
||||
test('a failing query throws BrewException with stderr', () async {
|
||||
final runner = FakeRunner(
|
||||
(_) => result('', exitCode: 1, stderr: 'Error: No available formula'),
|
||||
);
|
||||
final brew = BrewClient(
|
||||
brewPath: '/opt/homebrew/bin/brew',
|
||||
runner: runner.call,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => brew.info('nope', PackageKind.formula),
|
||||
throwsA(
|
||||
isA<BrewException>().having(
|
||||
(e) => e.stderr,
|
||||
'stderr',
|
||||
contains('No available formula'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'BrewTask streams stdout and stderr and records the exit code',
|
||||
() async {
|
||||
late Map<String, String> seenEnv;
|
||||
final brew = BrewClient(
|
||||
brewPath: '/opt/homebrew/bin/brew',
|
||||
askpassPath: '/tmp/askpass',
|
||||
starter: (exe, args, env) {
|
||||
seenEnv = env;
|
||||
return Process.start('/bin/sh', [
|
||||
'-c',
|
||||
'echo installing; echo warning >&2; exit 3',
|
||||
]);
|
||||
},
|
||||
);
|
||||
final task = brew.install('jq', PackageKind.formula);
|
||||
var notified = 0;
|
||||
task.addListener(() => notified++);
|
||||
|
||||
final code = await task.run();
|
||||
|
||||
expect(code, 3);
|
||||
expect(task.succeeded, isFalse);
|
||||
expect(task.running, isFalse);
|
||||
expect(task.lines.first, r'$ brew install --formula jq');
|
||||
expect(task.lines, containsAll(['installing', 'warning']));
|
||||
expect(notified, greaterThan(2));
|
||||
// Mutations may auto-update and may need sudo via the askpass helper.
|
||||
expect(seenEnv.containsKey('HOMEBREW_NO_AUTO_UPDATE'), isFalse);
|
||||
expect(seenEnv['SUDO_ASKPASS'], '/tmp/askpass');
|
||||
},
|
||||
);
|
||||
|
||||
test('upgrade builds the right arguments', () {
|
||||
final brew = BrewClient(brewPath: '/b/brew');
|
||||
expect(brew.upgrade().args, ['upgrade']);
|
||||
expect(brew.upgrade(greedy: true).args, ['upgrade', '--greedy']);
|
||||
expect(brew.upgrade(name: 'firefox', kind: PackageKind.cask).args, [
|
||||
'upgrade',
|
||||
'--cask',
|
||||
'firefox',
|
||||
]);
|
||||
expect(brew.addTap('a/b', url: '').args, ['tap', 'a/b']);
|
||||
expect(brew.addTap('a/b', url: 'https://x/y.git').args, [
|
||||
'tap',
|
||||
'a/b',
|
||||
'https://x/y.git',
|
||||
]);
|
||||
});
|
||||
|
||||
// Read-only checks against the real Homebrew on this machine, to catch
|
||||
// drift between brew's JSON and the parsers.
|
||||
group('real brew', () {
|
||||
final path = BrewClient.locate();
|
||||
final brew = path == null ? null : BrewClient(brewPath: path);
|
||||
|
||||
test('lists installed packages', () async {
|
||||
final installed = await brew!.installed();
|
||||
expect(installed, everyElement(isA<Package>()));
|
||||
expect(installed.where((p) => !p.isInstalled), isEmpty);
|
||||
}, skip: brew == null ? 'Homebrew not installed' : false);
|
||||
|
||||
test('lists taps including homebrew/core', () async {
|
||||
final taps = await brew!.taps();
|
||||
expect(taps.map((t) => t.name), contains('homebrew/core'));
|
||||
final core = taps.firstWhere((t) => t.name == 'homebrew/core');
|
||||
expect(core.formulaNames, contains('jq'));
|
||||
}, skip: brew == null ? 'Homebrew not installed' : false);
|
||||
|
||||
test('searches', () async {
|
||||
final r = await brew!.search('jq');
|
||||
expect(r.map((e) => e.name), contains('jq'));
|
||||
}, skip: brew == null ? 'Homebrew not installed' : false);
|
||||
|
||||
test('gets cask info', () async {
|
||||
final p = await brew!.info('firefox', PackageKind.cask);
|
||||
expect(p?.displayName, isNotEmpty);
|
||||
expect(jsonEncode(p?.name), '"firefox"');
|
||||
}, skip: brew == null ? 'Homebrew not installed' : false);
|
||||
});
|
||||
}
|
||||
58
test/fixtures/info_firefox.json
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"formulae": [],
|
||||
"casks": [
|
||||
{
|
||||
"token": "firefox",
|
||||
"full_token": "firefox",
|
||||
"old_tokens": [],
|
||||
"tap": "homebrew/cask",
|
||||
"name": [
|
||||
"Mozilla Firefox"
|
||||
],
|
||||
"desc": "Web browser",
|
||||
"homepage": "https://www.mozilla.org/firefox/",
|
||||
"url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/157.0.1/mac/en-US/Firefox%20157.0.1.dmg",
|
||||
"version": "157.0.1",
|
||||
"autobump": true,
|
||||
"no_autobump_message": null,
|
||||
"skip_livecheck": false,
|
||||
"installed": null,
|
||||
"installed_time": null,
|
||||
"bundle_version": null,
|
||||
"bundle_short_version": null,
|
||||
"pinned": false,
|
||||
"pinned_version": null,
|
||||
"outdated": false,
|
||||
"sha256": "40a0a649120635460256dae9ad63e40377f77326cbc9c449a5d0a1d7f5dd7982",
|
||||
"caveats": null,
|
||||
"caveats_rosetta": null,
|
||||
"depends_on": {
|
||||
"macos": {}
|
||||
},
|
||||
"conflicts_with": {
|
||||
"cask": [
|
||||
"firefox@beta",
|
||||
"firefox@cn",
|
||||
"firefox@esr"
|
||||
]
|
||||
},
|
||||
"container": null,
|
||||
"rename": [],
|
||||
"auto_updates": true,
|
||||
"deprecated": false,
|
||||
"deprecation_date": null,
|
||||
"deprecation_reason": null,
|
||||
"deprecation_replacement_formula": null,
|
||||
"deprecation_replacement_cask": null,
|
||||
"deprecate_args": null,
|
||||
"disabled": false,
|
||||
"disable_date": null,
|
||||
"disable_reason": null,
|
||||
"disable_replacement_formula": null,
|
||||
"disable_replacement_cask": null,
|
||||
"disable_args": null,
|
||||
"tap_git_head": "086dfbab8a72cd87eab20a8ff7e974db70051883",
|
||||
"ruby_source_path": "Casks/f/firefox.rb"
|
||||
}
|
||||
]
|
||||
}
|
||||
81
test/fixtures/info_jq.json
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"formulae": [
|
||||
{
|
||||
"name": "jq",
|
||||
"full_name": "jq",
|
||||
"tap": "homebrew/core",
|
||||
"oldnames": [],
|
||||
"aliases": [],
|
||||
"versioned_formulae": [],
|
||||
"desc": "Lightweight and flexible command-line JSON processor",
|
||||
"license": "MIT",
|
||||
"homepage": "https://jqlang.github.io/jq/",
|
||||
"versions": {
|
||||
"stable": "1.8.2",
|
||||
"head": "HEAD",
|
||||
"bottle": true
|
||||
},
|
||||
"patches": [],
|
||||
"revision": 0,
|
||||
"version_scheme": 0,
|
||||
"compatibility_version": null,
|
||||
"autobump": true,
|
||||
"no_autobump_message": null,
|
||||
"skip_livecheck": false,
|
||||
"pour_bottle_only_if": null,
|
||||
"keg_only": false,
|
||||
"keg_only_reason": null,
|
||||
"options": [],
|
||||
"build_dependencies": [],
|
||||
"dependencies": [
|
||||
"oniguruma"
|
||||
],
|
||||
"test_dependencies": [],
|
||||
"recommended_dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"uses_from_macos": [],
|
||||
"uses_from_macos_bounds": [],
|
||||
"conflicts_with": [],
|
||||
"conflicts_with_reasons": [],
|
||||
"link_overwrite": [],
|
||||
"caveats": null,
|
||||
"installed": [],
|
||||
"linked_keg": null,
|
||||
"pinned": false,
|
||||
"outdated": false,
|
||||
"deprecated": false,
|
||||
"deprecation_date": null,
|
||||
"deprecation_reason": null,
|
||||
"deprecation_replacement_formula": null,
|
||||
"deprecation_replacement_cask": null,
|
||||
"deprecate_args": null,
|
||||
"disabled": false,
|
||||
"disable_date": null,
|
||||
"disable_reason": null,
|
||||
"disable_replacement_formula": null,
|
||||
"disable_replacement_cask": null,
|
||||
"disable_args": null,
|
||||
"post_install_steps": [],
|
||||
"post_install_defined": false,
|
||||
"service": null,
|
||||
"tap_git_head": "3c09418e50ad51f7f241f4062ffcd7bffe36e3ce",
|
||||
"ruby_source_path": "Formula/j/jq.rb",
|
||||
"head_dependencies": {
|
||||
"build_dependencies": [
|
||||
"autoconf",
|
||||
"automake",
|
||||
"libtool"
|
||||
],
|
||||
"dependencies": [
|
||||
"oniguruma"
|
||||
],
|
||||
"test_dependencies": [],
|
||||
"recommended_dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"uses_from_macos": [],
|
||||
"uses_from_macos_bounds": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"casks": []
|
||||
}
|
||||
119
test/models_test.dart
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:taproom/brew/models.dart';
|
||||
|
||||
Map<String, dynamic> fixture(String name) =>
|
||||
jsonDecode(File('test/fixtures/$name').readAsStringSync())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
void main() {
|
||||
test('parses a formula from real brew info output', () {
|
||||
final p = Package.listFromInfoJson(fixture('info_jq.json')).single;
|
||||
expect(p.name, 'jq');
|
||||
expect(p.kind, PackageKind.formula);
|
||||
expect(p.tap, 'homebrew/core');
|
||||
expect(p.latestVersion, '1.8.2');
|
||||
expect(p.dependencies, ['oniguruma']);
|
||||
expect(p.isInstalled, isFalse);
|
||||
});
|
||||
|
||||
test('parses a cask from real brew info output', () {
|
||||
final p = Package.listFromInfoJson(fixture('info_firefox.json')).single;
|
||||
expect(p.name, 'firefox');
|
||||
expect(p.kind, PackageKind.cask);
|
||||
expect(p.displayName, 'Mozilla Firefox');
|
||||
expect(p.tap, 'homebrew/cask');
|
||||
expect(p.latestVersion, isNotNull);
|
||||
});
|
||||
|
||||
test('installed formula uses the newest install and on-request flag', () {
|
||||
final p = Package.fromFormulaJson({
|
||||
'name': 'git',
|
||||
'versions': {'stable': '2.50.0'},
|
||||
'installed': [
|
||||
{'version': '2.49.0', 'installed_on_request': false},
|
||||
{'version': '2.50.0', 'installed_on_request': true},
|
||||
],
|
||||
'outdated': false,
|
||||
'pinned': true,
|
||||
});
|
||||
expect(p.installedVersion, '2.50.0');
|
||||
expect(p.installedOnRequest, isTrue);
|
||||
expect(p.pinned, isTrue);
|
||||
expect(p.fullName, 'git');
|
||||
});
|
||||
|
||||
test('dependency-only formula is not installed on request', () {
|
||||
final p = Package.fromFormulaJson({
|
||||
'name': 'oniguruma',
|
||||
'installed': [
|
||||
{'version': '6.9.10', 'installed_on_request': false},
|
||||
],
|
||||
});
|
||||
expect(p.isInstalled, isTrue);
|
||||
expect(p.installedOnRequest, isFalse);
|
||||
});
|
||||
|
||||
test('parses outdated formulae and casks', () {
|
||||
final list = OutdatedEntry.listFromJson({
|
||||
'formulae': [
|
||||
{
|
||||
'name': 'node',
|
||||
'installed_versions': ['22.1.0'],
|
||||
'current_version': '22.2.0',
|
||||
'pinned': true,
|
||||
},
|
||||
],
|
||||
'casks': [
|
||||
{
|
||||
'name': 'firefox',
|
||||
'installed_versions': ['140.0'],
|
||||
'current_version': '141.0',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(list.map((e) => (e.name, e.kind, e.pinned)), [
|
||||
('node', PackageKind.formula, true),
|
||||
('firefox', PackageKind.cask, false),
|
||||
]);
|
||||
expect(list.last.installedVersions, ['140.0']);
|
||||
});
|
||||
|
||||
test('parses an API-backed tap', () {
|
||||
final t = Tap.fromJson({
|
||||
'name': 'homebrew/core',
|
||||
'path': '/opt/homebrew/Library/Taps/homebrew/homebrew-core',
|
||||
'installed': false,
|
||||
'official': true,
|
||||
'formula_names': ['a', 'b'],
|
||||
'cask_tokens': [],
|
||||
});
|
||||
expect(t.cloned, isFalse);
|
||||
expect(t.official, isTrue);
|
||||
expect(t.formulaNames, hasLength(2));
|
||||
expect(t.remote, isNull);
|
||||
});
|
||||
|
||||
test('parses a cloned third-party tap', () {
|
||||
// Shape from `brew tap-info --json hashicorp/tap`: custom_remote is a
|
||||
// bool, not the URL.
|
||||
final t = Tap.fromJson({
|
||||
'name': 'hashicorp/tap',
|
||||
'path': '/opt/homebrew/Library/Taps/hashicorp/homebrew-tap',
|
||||
'installed': true,
|
||||
'official': false,
|
||||
'remote': 'https://github.com/hashicorp/homebrew-tap',
|
||||
'custom_remote': false,
|
||||
'last_commit': '2 days ago',
|
||||
'branch': 'main',
|
||||
'formula_names': ['hashicorp/tap/terraform'],
|
||||
'cask_tokens': [],
|
||||
});
|
||||
expect(t.cloned, isTrue);
|
||||
expect(t.remote, 'https://github.com/hashicorp/homebrew-tap');
|
||||
expect(t.branch, 'main');
|
||||
expect(t.lastCommit, '2 days ago');
|
||||
});
|
||||
}
|
||||