Skip to content

Commit

Permalink
- Added collections storage implementation #102 (#105)
Browse files Browse the repository at this point in the history
- Reworked settings storage #101
  • Loading branch information
XFox111 authored Nov 21, 2022
1 parent 0d90a39 commit 3d392db
Show file tree
Hide file tree
Showing 13 changed files with 552 additions and 96 deletions.
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

0 comments on commit 3d392db

Please sign in to comment.