Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions macos_app/lib/providers/preferences_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ class PreferencesRepository {
// Settings
Future<Settings> getSettings() async {
try {
// Settings are not stored in the database currently, return default
return Settings();
return await _databaseService.getSettings();
} catch (e) {
debugPrint('Error getting settings: $e');
return Settings();
Expand All @@ -94,7 +93,7 @@ class PreferencesRepository {

Future<void> saveSettings(Settings settings) async {
try {
// Settings are not stored in the database currently, no-op
await _databaseService.saveSettings(settings);
} catch (e) {
debugPrint('Error saving settings: $e');
}
Expand All @@ -115,3 +114,4 @@ final preferencesRepositoryProvider = Provider<PreferencesRepository>(
return PreferencesRepository(databaseService);
},
);

74 changes: 74 additions & 0 deletions macos_app/lib/services/database_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,82 @@ class DatabaseService {
await _client.execute("DELETE FROM url_items");
await _client.execute("DELETE FROM categories");
}
Future<Settings> getSettings() async {
await initialize();
final ResultSet rs = await _client.query('SELECT value FROM settings WHERE key = ?', positional: ['settings']);
if (rs.rows.isEmpty) {
return Settings();
}
try {
final value = rs.rows.first['value'] as String?;
if (value == null) {
return Settings();
}
final Map<String, dynamic> json = jsonDecode(value);
return Settings.fromJson(json);
} catch (e) {
return Settings();
}
}

Future<void> saveSettings(Settings settings) async {
await initialize();
final jsonString = jsonEncode(settings.toJson());
await _client.execute(
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
positional: ['settings', jsonString],
);
}

Future<void> _createSettingsTable() async {
await initialize();
await _client.execute('''
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
''');
}

Future<void> initialize() async {
if (_isInitialized) return;

final dir = await getApplicationSupportDirectory();
final path = '${dir.path}/later.db';

_client = LibsqlClient(path);
await _client.connect();

// Create tables if they don't exist
await _client.execute('''
CREATE TABLE IF NOT EXISTS categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
''');

await _client.execute('''
CREATE TABLE IF NOT EXISTS url_items (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
categoryId TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (categoryId) REFERENCES categories (id) ON DELETE SET NULL
);
''');

await _createSettingsTable();

_isInitialized = true;
}

Future<void> close() async {
if (_isInitialized) {
await _client.close();
_isInitialized = false;

7 changes: 5 additions & 2 deletions macos_app/lib/utils/menubar.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:macos_ui/macos_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:local_notifier/local_notifier.dart';
Expand All @@ -9,6 +10,7 @@ import '../pages/settings_page.dart';
import '../pages/import_dialog.dart';
import '../pages/import_urls_dialog.dart';
import '../pages/export_dialog.dart';
import '../widgets/about_dialog.dart';
import '../providers/providers.dart';
import '../utils/import_export_manager.dart';

Expand Down Expand Up @@ -36,7 +38,7 @@ class LaterMenuBar {
'Later',
[
_buildMenuItem(context, 'About Later', null, () {
// TODO: Implement about dialog
showAboutLaterDialog(context);
}),
_buildMenuItem(context, 'Preferences', '⌘,', () {
Navigator.of(context).push(
Expand All @@ -45,7 +47,7 @@ class LaterMenuBar {
}),
const Divider(),
_buildMenuItem(context, 'Quit Later', '⌘Q', () {
// TODO: Implement graceful app exit
SystemNavigator.pop();
}),
],
),
Expand Down Expand Up @@ -224,3 +226,4 @@ class LaterMenuBar {
);
}
}

32 changes: 32 additions & 0 deletions macos_app/lib/widgets/about_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:macos_ui/macos_ui.dart';

/// A dialog that shows information about the Later app.
void showAboutLaterDialog(BuildContext context) {
showMacosAlertDialog(
context: context,
builder: (context) {
return MacosAlertDialog(
appIcon: const MacosIcon(
CupertinoIcons.info_circle,
size: 56,
),
title: const Text('About Later'),
message: const Text(
'A simple bookmarking app for macOS.',
textAlign: TextAlign.center,
),
primaryButton: PushButton(
controlSize: ControlSize.large,
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Close'),
),
);
},
);
}


199 changes: 0 additions & 199 deletions shared/js/popup.js
Original file line number Diff line number Diff line change
@@ -1,199 +0,0 @@
function generateId() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
},
);
}

function showStatus(message, type) {
const statusDiv = document.getElementById("status");
statusDiv.textContent = message;
statusDiv.className = "status " + type;

// Clear status after 3 seconds
setTimeout(function () {
statusDiv.className = "status";
}, 3000);
}

function initializePopup(browserAPI) {
// DOM elements
const categorySelect = document.getElementById("category");
const newCategoryForm = document.getElementById("newCategoryForm");
const newCategoryInput = document.getElementById("newCategory");
const createCategoryBtn = document.getElementById("createCategory");
const showNewCategoryLink = document.getElementById("showNewCategory");
const saveCurrentTabBtn = document.getElementById("saveCurrentTab");
const saveAllTabsBtn = document.getElementById("saveAllTabs");

// Load categories from storage
loadCategories();

// Event listeners
showNewCategoryLink.addEventListener("click", function (e) {
e.preventDefault();
newCategoryForm.classList.toggle("hidden");
showNewCategoryLink.classList.toggle("hidden");
newCategoryInput.focus();
});

createCategoryBtn.addEventListener("click", function () {
createCategory();
});

newCategoryInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
createCategory();
}
});

saveCurrentTabBtn.addEventListener("click", function () {
saveCurrentTab();
});

saveAllTabsBtn.addEventListener("click", function () {
saveAllTabs();
});

// Functions
function loadCategories() {
browserAPI.storage.sync.get("categories").then(function (data) {
let categories = data.categories || [];

// If no categories exist, create a default one
if (categories.length === 0) {
categories = [{ id: generateId(), name: "Bookmarks" }];
browserAPI.storage.sync.set({ categories: categories });
}

// Clear and populate the dropdown
categorySelect.innerHTML = "";
categories.forEach(function (category) {
const option = document.createElement("option");
option.value = category.id;
option.textContent = category.name;
categorySelect.appendChild(option);
});
}).catch(function (error) {
console.error("Error loading categories:", error);
});
}

function createCategory() {
const categoryName = newCategoryInput.value.trim();

if (categoryName) {
browserAPI.storage.sync.get("categories").then(function (data) {
const categories = data.categories || [];
const newCategory = {
id: generateId(),
name: categoryName,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

categories.push(newCategory);
browserAPI.storage.sync.set({ categories: categories }).then(function () {
// Reload categories and reset form
loadCategories();
newCategoryInput.value = "";
newCategoryForm.classList.add("hidden");
showNewCategoryLink.classList.remove("hidden");

// Select the new category
setTimeout(function () {
categorySelect.value = newCategory.id;
}, 100);
}).catch(function (error) {
console.error("Error setting categories:", error);
showStatus("Failed to create category", "error");
});
}).catch(function (error) {
console.error("Error getting categories:", error);
showStatus("Failed to create category", "error");
});
}
}

function saveCurrentTab() {
browserAPI.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
if (tabs.length === 0) {
showStatus("No active tab found.", "error");
return;
}
saveTabsToLater(tabs);
}).catch(function (error) {
console.error("Error querying tabs:", error);
showStatus("Failed to save current tab.", "error");
});
}

function saveAllTabs() {
browserAPI.tabs.query({ currentWindow: true }).then(function (tabs) {
if (tabs.length === 0) {
showStatus("No tabs found in the current window.", "error");
return;
}
saveTabsToLater(tabs);
}).catch(function (error) {
console.error("Error querying tabs:", error);
showStatus("Failed to save all tabs.", "error");
});
}

function saveTabsToLater(tabs) {
const urlItems = tabs.map((tab) => {
return {
id: generateId(),
url: tab.url,
title: tab.title || tab.url,
description: "",
categoryId: categorySelect.value,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
});

browserAPI.storage.sync.get("categories").then(function (data) {
const categories = data.categories || [];
const formattedCategories = categories.map((category) => {
return {
id: category.id,
name: category.name,
createdAt: category.createdAt || new Date().toISOString(),
updatedAt: category.updatedAt || new Date().toISOString(),
};
});

const exportData = {
urls: urlItems,
categories: formattedCategories,
version: "1.0.0",
exportedAt: new Date().toISOString(),
};

copyToClipboard(exportData, tabs.length, false);
triggerClipboardImport(exportData, tabs.length);
}).catch((error) => {
console.error("Error in saveTabsToLater:", error);
const fallbackExportData = {
urls: urlItems,
categories: [],
version: "1.0.0",
exportedAt: new Date().toISOString(),
};
copyToClipboard(fallbackExportData, tabs.length);
});
}

function triggerClipboardImport(exportData, tabCount) {
const laterUrl = `later:///clipboard-import`;
showStatus(`${tabCount} tab(s) sent to Later app.`, "success");
setTimeout(() => {
window.location.href = laterUrl;
}, 300);
}