Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added collections storage #102 #105

Merged
merged 1 commit into from
Nov 21, 2022
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@fluentui/react-components": "^9.7.0",
"@fluentui/react-icons": "^2.0.186",
"lzutf8": "^0.6.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.54.9",
Expand Down
5 changes: 5 additions & 0 deletions src/Models/Data/IGraphics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default interface IGraphics
{
Icon?: string;
Thumbnail?: string;
}
26 changes: 2 additions & 24 deletions src/Models/Data/TabModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,6 @@ export default class TabModel
*/
public ScrollPosition?: number;

/**
* Tab's thumbnail (optional)
*/
public Thumbnail?: string;

/**
* Tab's favicon (optional)
*/
public Icon?: string;

/**
* @param uri Tab's URL
*/
Expand All @@ -47,24 +37,12 @@ export default class TabModel
* @param uri Tab's URL
* @param title Tab's title
* @param scrollPoisition Tab's scroll position
* @param graphics Tab's graphics data
*/
constructor(uri: string, title: string, scrollPoisition: number, thumbnail: string);
constructor(uri: string, title?: string, scrollPosition?: number, thumbnail?: string)
constructor(uri: string, title: string, scrollPoisition: number);
constructor(uri: string, title?: string, scrollPosition?: number)
{
this.Url = uri;
this.Title = title;
this.Thumbnail = thumbnail;
this.ScrollPosition = scrollPosition;
}

public GetIcon(): string
{
if (this.Icon)
return this.Icon;

let url = new URL(this.Url);
url.pathname = "/favicon.ico";
return url.href;
}
}
3 changes: 2 additions & 1 deletion src/Models/Data/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import CollectionModel from "./CollectionModel";
import GroupModel from "./GroupModel";
import TabModel from "./TabModel";
import SettingsModel from "./SettingsModel";
import IGraphics from "./IGraphics";

export { SettingsModel, CollectionModel, GroupModel, TabModel };
export { SettingsModel, CollectionModel, GroupModel, TabModel, IGraphics };
1 change: 0 additions & 1 deletion src/Services/Storage/CollectionService.ts

This file was deleted.

159 changes: 159 additions & 0 deletions src/Services/Storage/CollectionsRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { compress, decompress } from "lzutf8";
import { Storage } from "webextension-polyfill";
import { CollectionModel } from "../../Models/Data";
import { ext } from "../../Utils";
import CollectionOptimizer from "../../Utils/CollectionOptimizer";

/**
* Data repository that provides access to saved collections.
*/
export default class CollectionsRepository
{
/**
* Fired when collections are changed.
*/
public ItemsChanged: (collections: CollectionModel[]) => void;

private source: Storage.StorageArea = null;

/**
* Generates a new instance of the class.
* @param source Storage area to be used
*/
public constructor(source: "sync" | "local")
{
this.source = source === "sync" ? ext?.storage.sync : ext?.storage.local;
ext?.storage.onChanged.addListener(this.OnStorageChanged);
}

/**
* Gets saved collections from repository.
* @returns Saved collections
*/
public async GetCollectionsAsync(): Promise<CollectionModel[]>
{
if (!this.source)
return [];

let chunks: { [key: string]: string; } = { };

// Setting which data to retrieve and its default value
// Saved collections are now stored in chunks. This is the most efficient way to store these.
for (let i = 0; i < 12; i++)
chunks[`chunk${i}`] = null;

chunks = await this.source.get(chunks);

let data: string = "";

for (let chunk of Object.values(chunks))
if (chunk)
data += chunk;

data = decompress(data, { inputEncoding: "StorageBinaryString" });

return CollectionOptimizer.DeserializeCollections(data);
}

/**
* Adds new collection to repository.
* @param collection Collection to be saved
*/
public async AddCollectionAsync(collection: CollectionModel): Promise<void>
{
if (!this.source)
return;

let items: CollectionModel[] = await this.GetCollectionsAsync();
items.push(collection);

await this.SaveChangesAsync(items);
}

/**
* Updates existing collection or adds a new one in repository.
* @param collection Collection to be updated
*/
public async UpdateCollectionAsync(collection: CollectionModel): Promise<void>
{
if (!this.source)
return;

let items: CollectionModel[] = await this.GetCollectionsAsync();
let index = items.findIndex(i => i.Timestamp === collection.Timestamp);

if (index === -1)
items.push(collection);
else
items[index] = collection;

await this.SaveChangesAsync(items);
}

/**
* Removes collection from repository.
* @param collection Collection to be removed
*/
public async RemoveCollectionAsync(collection: CollectionModel): Promise<void>
{
if (!this.source)
return;

let items: CollectionModel[] = await this.GetCollectionsAsync();
items = items.filter(i => i.Timestamp !== collection.Timestamp);

await this.SaveChangesAsync(items);
}

/**
* Removes all collections from repository.
*/
public async Clear(): Promise<void>
{
if (!this.source)
return;

let keys: string[] = [];

for (let i = 0; i < 12; i++)
keys.push(`chunk${i}`);

await this.source.remove(keys);
}

private async SaveChangesAsync(collections: CollectionModel[]): Promise<void>
{
if (!this.source)
return;

let data: string = CollectionOptimizer.SerializeCollections(collections);
data = compress(data, { outputEncoding: "StorageBinaryString" });

let chunks: string[] = CollectionOptimizer.SplitIntoChunks(data);

let items: { [key: string]: string; } = {};

for (let i = 0; i < chunks.length; i++)
items[`chunk${i}`] = chunks[i];

let chunksToDelete: string[] = [];

for (let i = chunks.length; i < 12; i++)
chunksToDelete.push(`chunk${i}`);

await this.source.set(items);
await this.source.remove(chunksToDelete);
}

private async OnStorageChanged(changes: { [key: string]: Storage.StorageChange }, areaName: string): Promise<void>
{
if (!this.source)
return;

if (!Object.keys(changes).some(k => k.startsWith("chunk")))
return;

let collections: CollectionModel[] = await this.GetCollectionsAsync();
this.ItemsChanged?.(collections);
}
}
63 changes: 63 additions & 0 deletions src/Services/Storage/GraphicsRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { IGraphics } from "../../Models/Data";
import { ext } from "../../Utils";

/**
* Provides access to saved graphics (icons and thumbnails).
*/
export default class GraphicsRepository
{
/**
* Gets saved graphics from storage.
* @returns Dictionary of IGraphics objects, where key is the URL of the graphics.
*/
public async GetGraphicsAsync(): Promise<Record<string, IGraphics>>
{
if (!ext)
return { };

let data: Record<string, any> = await ext.storage.local.get(null);
let graphics: Record<string, IGraphics> = { };

for (let key in data)
try
{
new URL(key);
graphics[key] = data[key] as IGraphics;
}
catch { continue; }

return graphics;
}

/**
* Saves graphics to storage.
* @param graphics Dictionary of IGraphics objects, where key is the URL of the graphics.
*/
public async AddOrUpdateGraphicsAsync(graphics: Record<string, IGraphics>): Promise<void>
{
if (!ext)
return;

let data: Record<string, any> = await ext.storage.local.get(Object.keys(graphics));

for (let key in graphics)
if (data[key] === undefined)
data[key] = graphics[key];
else
data[key] = { ...data[key], ...graphics[key] };

await ext.storage.local.set(graphics);
}

/**
* Removes graphics from storage.
* @param graphics Dictionary of IGraphics objects, where key is the URL of the graphics.
*/
public async RemoveGraphicsAsync(graphics: Record<string, IGraphics>): Promise<void>
{
if (!ext)
return;

await ext.storage.local.remove(Object.keys(graphics));
}
}
59 changes: 59 additions & 0 deletions src/Services/Storage/SettingsRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Storage } from "webextension-polyfill";
import { SettingsModel } from "../../Models/Data";
import { ext } from "../../Utils";

/**
* Data repository that provides access to saved settings.
*/
export default class SettingsRepository
{
/**
* Fired when settings are changed.
*/
public ItemsChanged: (changes: Partial<SettingsModel>) => void;

public constructor()
{
ext?.storage.sync.onChanged.addListener(this.OnStorageChanged);
}

/**
* Gets saved settings.
* @returns Saved settings
*/
public async GetSettingsAsync(): Promise<SettingsModel>
{
let fallbackOptions = new SettingsModel();

if (!ext)
return fallbackOptions;

let options: Record<string, any> = await ext.storage.sync.get(fallbackOptions);

return new SettingsModel(options);
}

/**
* Saves settings.
* @param changes Changes to be saved
*/
public async UpdateSettingsAsync(changes: Partial<SettingsModel>): Promise<void>
{
if (ext)
await ext.storage.sync.set(changes);
else if (this.ItemsChanged)
this.ItemsChanged(changes);
}

private OnStorageChanged(changes: { [key: string]: Storage.StorageChange }): void
{
let propsList: string[] = Object.keys(new SettingsRepository());
let settings: { [key: string]: any; } = {};

Object.entries(changes)
.filter(i => propsList.includes(i[0]))
.map(i => settings[i[0]] = i[1].newValue);

this.ItemsChanged?.(settings as Partial<SettingsModel>);
}
}
45 changes: 0 additions & 45 deletions src/Services/Storage/SettingsService.ts

This file was deleted.

Loading