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

Select engine to redirect #70

Merged
merged 5 commits into from
Feb 23, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,9 @@
<window key="window" title="Kagi for Safari" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="KagiForSafariWindowFrame" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
<rect key="contentRect" x="196" y="240" width="425" height="560"/>
<rect key="contentRect" x="196" y="240" width="425" height="590"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<value key="minSize" type="size" width="425" height="560"/>
<value key="minSize" type="size" width="425" height="590"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
</connections>
Expand All @@ -326,11 +326,11 @@
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Kagi_for_Safari" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="425" height="560"/>
<rect key="frame" x="0.0" y="0.0" width="425" height="590"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
<rect key="frame" x="0.0" y="0.0" width="425" height="560"/>
<rect key="frame" x="0.0" y="0.0" width="425" height="590"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
Expand Down
16 changes: 9 additions & 7 deletions safari/Universal/Kagi Search.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
3E1A51362B1734B70010A4C4 /* Kagi Search Extension iOS.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3ED5D48F2B0DE087009DDDAD /* Kagi Search Extension iOS.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3E1A513B2B180B890010A4C4 /* UpgradeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1A513A2B180B890010A4C4 /* UpgradeChecker.swift */; };
3E1A513C2B180CBE0010A4C4 /* UpgradeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1A513A2B180B890010A4C4 /* UpgradeChecker.swift */; };
3E7B281A2B86D220005EB620 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 3E7B28192B86D220005EB620 /* manifest.json */; };
3E9DF5082B3E3E70005DF2C3 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 3ED5D55B2B0DE720009DDDAD /* manifest.json */; };
3E9DF5092B3E3E71005DF2C3 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 3ED5D55B2B0DE720009DDDAD /* manifest.json */; };
3EB8FA422B61F6740052E8D4 /* Deeplinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB8FA412B61F6740052E8D4 /* Deeplinks.swift */; };
3EB8FA432B61F6740052E8D4 /* Deeplinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB8FA412B61F6740052E8D4 /* Deeplinks.swift */; };
3EB8FA472B6231DC0052E8D4 /* Settings_Animation_dark.gif in Resources */ = {isa = PBXBuildFile; fileRef = 3EB8FA452B6231DB0052E8D4 /* Settings_Animation_dark.gif */; };
Expand Down Expand Up @@ -133,6 +133,7 @@

/* Begin PBXFileReference section */
3E1A513A2B180B890010A4C4 /* UpgradeChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeChecker.swift; sourceTree = "<group>"; };
3E7B28192B86D220005EB620 /* manifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = manifest.json; sourceTree = "<group>"; };
3EB8FA412B61F6740052E8D4 /* Deeplinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deeplinks.swift; sourceTree = "<group>"; };
3EB8FA452B6231DB0052E8D4 /* Settings_Animation_dark.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = Settings_Animation_dark.gif; sourceTree = "<group>"; };
3EB8FA462B6231DB0052E8D4 /* Settings_Animation_light.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = Settings_Animation_light.gif; sourceTree = "<group>"; };
Expand All @@ -141,7 +142,7 @@
3EC41D232B21B587009B17FE /* macOS Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "macOS Assets.xcassets"; sourceTree = "<group>"; };
3EC772502B2C48CE00E40354 /* Kagi Search iOS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Kagi Search iOS copy-Info.plist"; path = "/Users/nano/Kagi/kagi-for-safari-macos/Kagi Search iOS copy-Info.plist"; sourceTree = "<absolute>"; };
3EC772622B2C492F00E40354 /* Kagi Search Extension iOS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Kagi Search Extension iOS copy-Info.plist"; path = "/Users/nano/Kagi/kagi-for-safari-macos/Kagi Search Extension iOS copy-Info.plist"; sourceTree = "<absolute>"; };
3EC772662B2F904200E40354 /* Kagi-Search-macOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Kagi-Search-macOS-Info.plist"; sourceTree = "<group>"; };
3EC772662B2F904200E40354 /* Kagi-Search-macOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Kagi-Search-macOS-Info.plist"; sourceTree = "<group>"; };
3ED5D48F2B0DE087009DDDAD /* Kagi Search Extension iOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Kagi Search Extension iOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
3ED5D4AC2B0DE2BD009DDDAD /* Kagi Search Extension macOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Kagi Search Extension macOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
3ED5D4C92B0DE324009DDDAD /* Kagi Search Extension (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Kagi Search Extension (iOS).entitlements"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -297,6 +298,7 @@
3ED5D4FE2B0DE6A5009DDDAD /* Kagi Search (macOS) */ = {
isa = PBXGroup;
children = (
3EC772662B2F904200E40354 /* Kagi-Search-macOS-Info.plist */,
3ED5D4FF2B0DE6A5009DDDAD /* NSWindow+Helpers.swift */,
3ED5D5002B0DE6A5009DDDAD /* Main.storyboard */,
3ED5D5022B0DE6A5009DDDAD /* AppDelegate.swift */,
Expand Down Expand Up @@ -341,7 +343,6 @@
3ED5D5552B0DE720009DDDAD /* popup.js */,
3ED5D5562B0DE720009DDDAD /* background.js */,
3ED5D55A2B0DE720009DDDAD /* popup.html */,
3ED5D55B2B0DE720009DDDAD /* manifest.json */,
3ED5D55C2B0DE720009DDDAD /* popup.css */,
);
path = Resources;
Expand All @@ -350,6 +351,7 @@
3ED5D5772B0DE75F009DDDAD /* iOS (Extension) */ = {
isa = PBXGroup;
children = (
3ED5D55B2B0DE720009DDDAD /* manifest.json */,
3ED5D4C92B0DE324009DDDAD /* Kagi Search Extension (iOS).entitlements */,
3ED5D5782B0DE75F009DDDAD /* Info.plist */,
);
Expand All @@ -359,6 +361,7 @@
3ED5D5792B0DE75F009DDDAD /* macOS (Extension) */ = {
isa = PBXGroup;
children = (
3E7B28192B86D220005EB620 /* manifest.json */,
3ED5D57A2B0DE75F009DDDAD /* Kagi Search Extension (macOS).entitlements */,
3ED5D57B2B0DE75F009DDDAD /* Info.plist */,
3E1A513A2B180B890010A4C4 /* UpgradeChecker.swift */,
Expand All @@ -369,7 +372,6 @@
C31E69142823DD4E00B1491B = {
isa = PBXGroup;
children = (
3EC772662B2F904200E40354 /* Kagi-Search-macOS-Info.plist */,
C39A9F7028588C0100E4C0A3 /* MainConfig.xcconfig */,
3ED5D5772B0DE75F009DDDAD /* iOS (Extension) */,
3ED5D5792B0DE75F009DDDAD /* macOS (Extension) */,
Expand Down Expand Up @@ -599,9 +601,9 @@
buildActionMask = 2147483647;
files = (
3ED5D5722B0DE720009DDDAD /* popup.css in Resources */,
3E9DF5092B3E3E71005DF2C3 /* manifest.json in Resources */,
3ED5D58F2B0DFC34009DDDAD /* _locales in Resources */,
3ED5D56E2B0DE720009DDDAD /* popup.html in Resources */,
3E7B281A2B86D220005EB620 /* manifest.json in Resources */,
3ED5D5682B0DE720009DDDAD /* background.js in Resources */,
3ED5D5922B0DFC81009DDDAD /* images in Resources */,
3ED5D5662B0DE720009DDDAD /* popup.js in Resources */,
Expand Down Expand Up @@ -1277,7 +1279,7 @@
"DEVELOPMENT_TEAM[sdk=macosx*]" = TFVG979488;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Kagi-Search-macOS-Info.plist";
INFOPLIST_FILE = "Kagi Search (macOS)/Kagi-Search-macOS-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Kagi Search is a quick, user-centric, 100% privacy-respecting search engine with results augmented by non-commercial indexes and personalized searches.";
Expand Down Expand Up @@ -1320,7 +1322,7 @@
"DEVELOPMENT_TEAM[sdk=macosx*]" = TFVG979488;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Kagi-Search-macOS-Info.plist";
INFOPLIST_FILE = "Kagi Search (macOS)/Kagi-Search-macOS-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Kagi Search is a quick, user-centric, 100% privacy-respecting search engine with results augmented by non-commercial indexes and personalized searches.";
Expand Down
4 changes: 2 additions & 2 deletions safari/Universal/MainConfig.xcconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
MARKETING_VERSION = 2.2.1
CURRENT_PROJECT_VERSION = 22 // this needs to be increased with each version change as well (not set to 1 when version is updated)
MARKETING_VERSION = 2.2.2
CURRENT_PROJECT_VERSION = 24 // this needs to be increased with each version change as well (not set to 1 when version is updated)
PRODUCT_NAME = Kagi for Safari
3 changes: 3 additions & 0 deletions safari/Universal/Shared (App)/Base.lproj/Main.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ <h2>Instructions</h2>
<img class="platform-ios instruction-animation lightmode" src="../Grant_Permissions_Animation_light.gif" />
<img class="platform-ios instruction-animation darkmode" src="../Grant_Permissions_Animation_dark.gif" />
<ol class="instructions" start="3">
<li>
<p><em>Optional:</em> Select the search engine you want to redirect to Kagi in the extension popup.</p>
</li>
<li>
<strong>To enable</strong> Kagi in Private Browsing, <span class="platform-mac">check the "Allow in Private Browsing" checkbox</span><span class="platform-ios">enable the "Private Browsing" toggle switch</span>. Then copy your Kagi private session link from <a href="https://kagi.com/settings?p=user_details#sessionLink" target="_new">your Kagi account settings</a> into the extension popup.
<a href="../allow_private_browsing.png" class="screenshot platform-mac"><object type="image/svg+xml" data="../ScreenshotIcon.svg"></object></a>
Expand Down
70 changes: 53 additions & 17 deletions safari/Universal/Shared (Extension)/Resources/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,27 @@ const yahooUrls = {
"search.yahoo.com": "p"
};
const builtInEngines = Object.assign({}, googleUrls, yandexUrls, ddgUrls, bingUrls, baiduUrls, sogouUrls, ecosiaUrls, yahooUrls);
const domainMap = {
"Google": ["google.com.au", "google.md", "google.ru", "google.me", "google.com.qa", "google.com.gt", "google.se", "google.tm", "google.vg", "google.it", "google.cat", "google.com.ru", "google.com.gr", "google.ee", "google.cd", "google.sk", "google.com.ly", "google.hn", "google.co.jp", "google.ad", "google.com.sg", "google.ie", "google.co.vi", "google.kg", "google.com.kh", "google.co.ck", "google.is", "google.tt", "google.vu", "google.bg", "google.ch", "google.com.sa", "google.tn", "google.pl", "google.ro", "google.gm", "google.tl", "google.mg", "google.dk", "google.com.bo", "google.je", "google.com.kw", "google.dz", "google.ga", "google.com.gh", "google.lt", "google.com.ag", "google.ps", "google.com.vc", "google.com.pr", "google.co.cr", "google.pn", "google.com.tr", "google.sn", "google.tg", "google.gg", "google.gr", "google.com.mt", "google.nu", "google.cm", "google.lk", "google.co.mz", "google.cv", "google.sm", "google.no", "google.al", "google.bi", "google.com.af", "google.sr", "google.jo", "google.sh", "google.co.uk", "google.co.bw", "google.dm", "google.at", "google.co.ug", "google.dj", "google.si", "google.com.pg", "google.com.tj", "google.co.za", "google.nl", "google.sc", "google.ae", "google.mv", "google.ne", "google.gy", "google.com.sl", "google.co.in", "google.com.bn", "google.ht", "google.com.ua", "google.com.my", "google.co.kr", "google.com", "google.by", "google.com.cu", "google.com.lb", "google.co.nz", "google.mu", "google.com.om", "google.as", "google.com.pe", "google.mk", "google.td", "google.es", "google.az", "google.com.hk", "google.com.do", "google.bt", "google.am", "google.fm", "google.com.mx", "google.fi", "google.com.bz", "google.st", "google.com.vn", "google.rs", "google.bs", "google.cn", "google.com.pa", "google.com.sb", "google.lv", "google.co.uz", "google.co.hu", "google.co.ve", "google.co.zw", "google.com.ai", "google.com.co", "google.ci", "google.com.uy", "google.cl", "google.mw", "google.cz", "google.co.il", "google.co.th", "google.be", "google.hr", "google.fr", "google.im", "google.com.ec", "google.cg", "google.iq", "google.com.np", "google.gl", "google.co.ke", "google.co.id", "google.ml", "google.ms", "google.com.ni", "google.mn", "google.ki", "google.lu", "google.hu", "google.rw", "google.co.ma", "google.com.tw", "google.co.ls", "google.com.et", "google.li", "google.com.br", "google.bj", "google.com.py", "google.co.tz", "google.ba", "google.co.ao", "google.bf", "google.com.ph", "google.com.sv", "google.com.bd", "google.com.mm", "google.la", "google.ws", "google.com.fj", "google.co.zm", "google.cf", "google.nr", "google.to", "google.com.jm", "google.com.ar", "google.com.gi", "google.ca", "google.kz", "google.com.cy", "google.de", "google.com.na", "google.com.pk", "google.pt", "google.ge", "google.so", "google.com.bh", "google.com.eg", "google.com.ng"],
"DuckDuckGo": ["duckduckgo.com", "duckduckgo.pl", "duckduckgo.jp", "duckduckgo.co", "duckduckco.de", "duckduckgo.ca", "duckduckgo.co.uk", "duckduckgo.com.mx", "duckduckgo.com.tw", "duckduckgo.dk", "duckduckgo.in", "duckduckgo.ke", "duckduckgo.mx", "duckduckgo.nl", "duckduckgo.org", "duckduckgo.sg", "duckduckgo.uk", "duckgo.com", "ddg.co", "ddg.gg", "duck.co", "duck.com"],
"Yahoo": ["search.yahoo.com"],
"Ecosia": ["ecosia.org"],
"Bing": ["bing.com"],
"Sogou": ["m.so.com", "so.com", "sogou.com", "m.sogou.com"],
"Baidu": ["baidu.com", "m.baidu.com"],
"Yandex": ["yandex.ru", "yandex.org", "yandex.net", "yandex.net.ru", "yandex.com.ru", "yandex.ua", "yandex.com.ua", "yandex.by", "yandex.eu", "yandex.ee", "yandex.lt", "yandex.lv", "yandex.md", "yandex.uz", "yandex.mx", "yandex.do", "yandex.tm", "yandex.de", "yandex.ie", "yandex.in", "yandex.qa", "yandex.so", "yandex.nu", "yandex.tj", "yandex.dk", "yandex.es", "yandex.pt", "yandex.pl", "yandex.lu", "yandex.it", "yandex.az", "yandex.ro", "yandex.rs", "yandex.sk", "yandex.no", "ya.ru", "yandex.com", "yandex.asia", "yandex.mobi"]
};
function domainKeyForHost(knownHost) {
let domainKeys = Object.keys(domainMap);
for (let i=0; i<domainKeys.length; i++) {
let domainKey = domainKeys[i];
if (domainMap[domainKey].indexOf(knownHost) > -1) {
return domainKey;
}
}
return "";
}
const supportedEngineNames = Object.keys(domainMap);
const www = "www.";
const yahoo = "search.yahoo.com";
const extensionId = "com.kagi.Kagi-Search-for-Safari.Extension (TFVG979488)";
Expand All @@ -288,10 +309,11 @@ var ua = {},
os = !0,
rs = !0,
currentEngine = "All",
defaultEngineToRedirect = "Google",
defaultKagiSearchTemplate = "https://kagi.com/search?q=%s",
kagiSearchTemplate = defaultKagiSearchTemplate,
kagiPrivateSearchTemplate = "",
flagCheckedLocalStorageForPrivateSessionLink = false,
flagFetchedPreferences = false,
customURLMode = 0,
customURLList = [],
regularTabIds = [],
Expand All @@ -317,7 +339,8 @@ function captureQuery(a) {
b.endsWith(yahoo) && (b = yahoo);
const path = a.pathname;
var shouldBlockGoogleNonSearch = (b in googleUrls && !(path.startsWith("/search")));
if (b in builtInEngines && !(shouldBlockGoogleNonSearch) && (a = (new URLSearchParams(a.search)).get(builtInEngines[b]))) return a;
var shouldBlockRedirectBasedOnUserPreference = ([currentEngine, "All"].indexOf(domainKeyForHost(b)) < 0);
if (b in builtInEngines && !(shouldBlockGoogleNonSearch || shouldBlockRedirectBasedOnUserPreference) && (a = (new URLSearchParams(a.search)).get(builtInEngines[b]))) return a;
}

function rewriteQueryURL(a, b) {
Expand All @@ -341,10 +364,10 @@ function rewriteQueryURL(a, b) {
}
var tk = 0;
function checkForSearch(a) {
if (!flagCheckedLocalStorageForPrivateSessionLink) {
console.log("[checkForSearch] Search query started before local private session link was fetched");
checkLocalStorageForPrivateSessionLink(function(){
console.log("[checkForSearch] Fetched local private session link as part of first search query during current browsing session");
if (!flagFetchedPreferences) {
console.log("[checkForSearch] Search query started before preferences were fetched");
getPreferencesFromStorage(function(){
console.log("[checkForSearch] Fetched preferences as part of first search query during current browsing session");
_checkForSearch(a);
});
} else {
Expand Down Expand Up @@ -414,13 +437,23 @@ function updatePrivateSessionLink(link) {
}
}

function checkLocalStorageForPrivateSessionLink(callback) {
browser.storage.local.get("kagiPrivateSessionLink", function(value) {
function getPreferencesFromStorage(callback) {
browser.storage.local.get(["kagiPrivateSessionLink","kagiEngineToRedirect"], function(value) {
// Private session link
var link = value.kagiPrivateSessionLink;
if (typeof (link) !== "undefined") {
if (typeof (link) == "string") {
updatePrivateSessionLink(link);
}
flagCheckedLocalStorageForPrivateSessionLink = true;
// Engine to redirect
var engine = value.kagiEngineToRedirect;
if (typeof (engine) == "string") {
if (engine == "All" || supportedEngineNames.indexOf(engine) < 0) {
currentEngine = defaultEngineToRedirect; // default to redirecting Google
} else {
currentEngine = engine;
}
}
flagFetchedPreferences = true;
callback();
});
}
Expand Down Expand Up @@ -454,18 +487,21 @@ browser.runtime.onInstalled.addListener(function(details) {
});


// Check for a private session link at startup so that the first search
// Check for a private session link and default engine at startup so that the first search
// in a private window or tab doesn't fail
checkLocalStorageForPrivateSessionLink(function(){
console.log("Finished startup check for local private session link");
getPreferencesFromStorage(function(){
console.log("Finished fetching preferences on startup");
});

function messageReceived(data, sender) {
let updatedKagiPrivateSessionLink = data["updatedKagiPrivateSessionLink"];
if (stringIsValid(updatedKagiPrivateSessionLink)) {
return updatePrivateSessionLink(updatedKagiPrivateSessionLink)
.then(() => Promise.resolve(true))
.catch((error) => Promise.reject(error));
let updatedKagiEngineToRedirect = data["updatedKagiEngineToRedirect"];
if (stringIsValid(updatedKagiPrivateSessionLink) || stringIsValid(updatedKagiEngineToRedirect)) {
// FIXME: Decide whether to send the session link and engine choice through storage or through the message itself. Right now we're doing both.
getPreferencesFromStorage(function(){
console.log("Finished fetching preferences after receiving an update message from popup.js");
});
return Promise.resolve(true);
} else {
return false;
}
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading