macos_security_scoped_bookmarks 0.1.0
macos_security_scoped_bookmarks: ^0.1.0 copied to clipboard
Token-based macOS security-scoped bookmark creation, resolution, and access lifetimes.
import 'dart:async';
import 'dart:io';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:macos_security_scoped_bookmarks/macos_security_scoped_bookmarks.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(const BookmarkExampleApp());
const _bookmarkPreferenceKey = 'securityScopedBookmark';
abstract interface class BookmarkStore {
Future<MacOSSecurityScopedBookmark?> load();
Future<void> save(MacOSSecurityScopedBookmark bookmark);
}
final class PreferencesBookmarkStore implements BookmarkStore {
const PreferencesBookmarkStore();
@override
Future<MacOSSecurityScopedBookmark?> load() async {
final encoded = await SharedPreferencesAsync().getString(
_bookmarkPreferenceKey,
);
if (encoded == null) {
return null;
}
try {
return MacOSSecurityScopedBookmark.decode(encoded);
} on FormatException {
return null;
}
}
@override
Future<void> save(final MacOSSecurityScopedBookmark bookmark) {
return SharedPreferencesAsync().setString(
_bookmarkPreferenceKey,
bookmark.encode(),
);
}
}
class BookmarkExampleApp extends StatelessWidget {
const BookmarkExampleApp({
super.key,
this.store = const PreferencesBookmarkStore(),
});
final BookmarkStore store;
@override
Widget build(final BuildContext context) {
return MaterialApp(
title: 'Security-Scoped Bookmarks',
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: BookmarkExamplePage(store: store),
);
}
}
class BookmarkExamplePage extends StatefulWidget {
const BookmarkExamplePage({required this.store, super.key});
final BookmarkStore store;
@override
State<BookmarkExamplePage> createState() => _BookmarkExamplePageState();
}
class _BookmarkExamplePageState extends State<BookmarkExamplePage> {
MacOSSecurityScopedBookmark? _bookmark;
bool _isRestoring = true;
String _status = 'Restoring any previously saved bookmark…';
@override
void initState() {
super.initState();
unawaited(_restoreBookmark());
}
Future<void> _restoreBookmark() async {
MacOSSecurityScopedBookmark? bookmark;
Object? restoreError;
try {
bookmark = await widget.store.load();
} on Object catch (error) {
restoreError = error;
}
if (!mounted) {
return;
}
setState(() {
_isRestoring = false;
_bookmark = bookmark;
_status =
restoreError != null
? 'Could not restore bookmark: $restoreError'
: bookmark == null
? 'Choose a file to create a read-only bookmark.'
: 'Restored a bookmark saved during an earlier launch.';
});
}
Future<void> _chooseFile() async {
try {
final selection = await openFile();
if (selection == null) {
return;
}
const access = MacOSSecurityScopedBookmarkAccess.readOnly;
final bookmark = await createBookmark(
File(selection.path),
access: access,
);
await widget.store.save(bookmark);
if (!mounted) {
return;
}
setState(() {
_bookmark = bookmark;
_status = 'Bookmark created for ${selection.name}.';
});
} on Object catch (error) {
if (!mounted) {
return;
}
setState(() => _status = 'Could not create bookmark: $error');
}
}
Future<void> _resolveAndRead() async {
final bookmark = _bookmark;
if (bookmark == null) {
return;
}
try {
final byteCount = await withBookmarkAccess(bookmark, (
final resource,
) async {
if (resource.isStale) {
final refreshedBookmark = await resource.refreshBookmark();
await widget.store.save(refreshedBookmark);
_bookmark = refreshedBookmark;
}
return (resource.entity as File).length();
});
if (!mounted) {
return;
}
setState(() {
_status = 'Read access succeeded: $byteCount bytes.';
});
} on Object catch (error) {
if (!mounted) {
return;
}
setState(() => _status = 'Could not access bookmark: $error');
}
}
@override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Security-Scoped Bookmarks')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(_status, textAlign: TextAlign.center),
const SizedBox(height: 24),
FilledButton(
onPressed: _isRestoring ? null : _chooseFile,
child: const Text('Choose file and create bookmark'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed:
_isRestoring || _bookmark == null
? null
: _resolveAndRead,
child: const Text('Resolve bookmark and read file'),
),
],
),
),
),
),
);
}
}