You've already forked tf2wikipricing
bump version to 0.8.0
Reviewed-on: http://charon.local/git/xen/tf2wikipricing/pulls/21
This commit is contained in:
@@ -23,13 +23,14 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
- name: Test project
|
- name: Test UserScript build
|
||||||
run: bun test --define __VERSION__='${{ steps.version.outputs.version }}' --define __EXTENSION_NAME='"tf2wikipricing"'
|
run: bun test --define __VERSION__='${{ steps.version.outputs.version }}' --define __EXTENSION_NAME='"tf2wikipricing"' --define __ENV_USERSCRIPT=1 --define __ENV_WEBEXTENSION=0
|
||||||
|
- name: Test WebExtension build
|
||||||
|
run: bun test --define __VERSION__='${{ steps.version.outputs.version }}' --define __EXTENSION_NAME='"tf2wikipricing"' --define __ENV_USERSCRIPT=0 --define __ENV_WEBEXTENSION=1
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: bun run build
|
run: bun run build
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: tf2wikipricing.user.js
|
name: tf2wikipricing
|
||||||
path: |
|
path: dist/
|
||||||
dist/userscript/tf2wikipricing.user.js
|
|
||||||
@@ -19,16 +19,17 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
- name: Test project
|
- name: Test UserScript build
|
||||||
run: bun test --define __VERSION__='${{ steps.version.outputs.version }}' --define __EXTENSION_NAME='"tf2wikipricing"'
|
run: bun test --define __VERSION__='${{ steps.version.outputs.version }}' --define __EXTENSION_NAME='"tf2wikipricing"' --define __ENV_USERSCRIPT=1 --define __ENV_WEBEXTENSION=0
|
||||||
|
- name: Test WebExtension build
|
||||||
|
run: bun test --define __VERSION__='${{ steps.version.outputs.version }}' --define __EXTENSION_NAME='"tf2wikipricing"' --define __ENV_USERSCRIPT=0 --define __ENV_WEBEXTENSION=1
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: bun run build --mode production
|
run: bun run build --mode production
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: tf2wikipricing.user.js
|
name: tf2wikipricing
|
||||||
path: |
|
path: dist/
|
||||||
dist/userscript/tf2wikipricing.user.js
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: debian-latest
|
runs-on: debian-latest
|
||||||
needs: build
|
needs: build
|
||||||
@@ -36,8 +37,14 @@ jobs:
|
|||||||
- name: Download release artifacts
|
- name: Download release artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: tf2wikipricing.user.js
|
name: tf2wikipricing
|
||||||
path: userscript
|
path: dist/
|
||||||
|
- name: Package Chrome extension
|
||||||
|
run: |
|
||||||
|
cd dist/
|
||||||
|
echo "${{ secrets.CRX_PRIVATE_KEY }}" > private.pem
|
||||||
|
bun x crx pack -p private.pem -o tf2wikipricing.crx extension/
|
||||||
|
rm -f private.pem
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: use-go-action
|
id: use-go-action
|
||||||
uses: akkuman/gitea-release-action@v1
|
uses: akkuman/gitea-release-action@v1
|
||||||
@@ -45,5 +52,6 @@ jobs:
|
|||||||
title: "v${{ need.build.outputs.version }}"
|
title: "v${{ need.build.outputs.version }}"
|
||||||
name: "v${{ need.build.outputs.version }}"
|
name: "v${{ need.build.outputs.version }}"
|
||||||
files: |
|
files: |
|
||||||
userscript/**
|
dist/tf2wikipricing.crx
|
||||||
|
dist/tf2wikipricing.user.js
|
||||||
sha256sum: true
|
sha256sum: true
|
||||||
@@ -57,7 +57,7 @@ describe('prepareExchangeRates', () => {
|
|||||||
|
|
||||||
const rates = await prepareExchangeRates();
|
const rates = await prepareExchangeRates();
|
||||||
expect(rates).toEqual(mockRates);
|
expect(rates).toEqual(mockRates);
|
||||||
expect(GM_fetch).not.toHaveBeenCalled();
|
expect(GM_fetch as jest.Mock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch new rates when they are expired', async () => {
|
it('should fetch new rates when they are expired', async () => {
|
||||||
@@ -66,17 +66,18 @@ describe('prepareExchangeRates', () => {
|
|||||||
if (key === storage_exchangerates_next) return new Date(Date.now() - 50000).toISOString();
|
if (key === storage_exchangerates_next) return new Date(Date.now() - 50000).toISOString();
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
const mockResponse = {
|
||||||
|
rates: mockRates,
|
||||||
|
time_next_update_utc: new Date(Date.now() + 100000).toISOString()
|
||||||
|
};
|
||||||
(GM_fetch as jest.Mock).mockResolvedValue({
|
(GM_fetch as jest.Mock).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => (mockResponse)
|
||||||
rates: mockRates,
|
|
||||||
time_next_update_utc: new Date(Date.now() + 100000).toISOString()
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockResponse);
|
||||||
|
|
||||||
const rates = await prepareExchangeRates();
|
const rates = await prepareExchangeRates();
|
||||||
expect(rates).toEqual(mockRates);
|
expect(rates).toEqual(mockRates);
|
||||||
expect(GM_fetch).toHaveBeenCalled();
|
|
||||||
expect(setStorageValue).toHaveBeenCalledWith(storage_exchangerates, mockRates);
|
expect(setStorageValue).toHaveBeenCalledWith(storage_exchangerates, mockRates);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ describe('prepareExchangeRates', () => {
|
|||||||
ok: false,
|
ok: false,
|
||||||
status: 500
|
status: 500
|
||||||
} as Response);
|
} as Response);
|
||||||
|
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => {});
|
||||||
|
|
||||||
const rates = await prepareExchangeRates();
|
const rates = await prepareExchangeRates();
|
||||||
expect(rates).toBeNull();
|
expect(rates).toBeNull();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe('Price Service', () => {
|
|||||||
|
|
||||||
const mockCachedData: ItemPriceData = {
|
const mockCachedData: ItemPriceData = {
|
||||||
sku: mockSku,
|
sku: mockSku,
|
||||||
update: new Date(Date.now() - 15 * 60 * 1000), // 15 minutes ago
|
update: new Date(Date.now() - 15 * 60 * 1000).getTime(), // 15 minutes ago
|
||||||
ttl: mockTtl,
|
ttl: mockTtl,
|
||||||
keys: 1,
|
keys: 1,
|
||||||
metal: 21.11,
|
metal: 21.11,
|
||||||
@@ -56,37 +56,37 @@ describe('Price Service', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('fetchPrice fetches new data when cache is expired', async () => {
|
test('fetchPrice fetches new data when cache is expired', async () => {
|
||||||
const expiredCache: ItemPriceData = { ...mockCachedData, update: new Date(Date.now() - 2 * mockTtl) };
|
const expiredCache: ItemPriceData = { ...mockCachedData, update: new Date(Date.now() - 2 * mockTtl).getTime() };
|
||||||
(getStorageValue as jest.Mock).mockResolvedValue(expiredCache);
|
(getStorageValue as jest.Mock).mockResolvedValue(expiredCache);
|
||||||
(priceUsingPricesTF as jest.Mock).mockResolvedValue(mockPriceResponse)
|
(priceUsingPricesTF as jest.Mock).mockResolvedValue(mockPriceResponse);
|
||||||
|
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockPriceResponse);
|
||||||
|
|
||||||
const result = await fetchPrice(mockToken, mockDefIndex + ";" + mockQuality)
|
const result = await fetchPrice(mockToken, mockDefIndex + ";" + mockQuality)
|
||||||
|
|
||||||
expect(priceUsingPricesTF).toHaveBeenCalledWith(mockToken, `${mockDefIndex};${mockQuality}`)
|
|
||||||
expect(setStorageValue).toHaveBeenCalled()
|
expect(setStorageValue).toHaveBeenCalled()
|
||||||
expect(result.metal).not.toBe(mockCachedData.metal)
|
expect(result.metal).not.toBe(mockCachedData.metal)
|
||||||
expect(result.metal).toBe(mockPriceResponse.metal)
|
expect(result.metal).toBe(mockPriceResponse.metal)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fetchPrice rejects with 401 when no token provided', async () => {
|
test('fetchPrice rejects with 401 when no token provided', async () => {
|
||||||
await expect(fetchPrice('', mockDefIndex + ";" + mockQuality)).rejects.toBe(401)
|
await expect(fetchPrice('', mockDefIndex + ";" + mockQuality)).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fetchPrice handles pricing API errors', async () => {
|
test('fetchPrice handles pricing API errors', async () => {
|
||||||
const testError = 500;
|
(chrome.runtime.sendMessage as jest.Fn).mockResolvedValue(null);
|
||||||
(priceUsingPricesTF as jest.Mock).mockRejectedValue(testError);
|
(priceUsingPricesTF as jest.Mock).mockImplementation(() => Promise.reject(new Error('500 Internal Server Error')));
|
||||||
(getStorageValue as jest.Mock).mockResolvedValue(null)
|
(getStorageValue as jest.Mock).mockResolvedValue(null)
|
||||||
|
|
||||||
await expect(fetchPrice(mockToken, mockDefIndex + ";" + mockQuality)).rejects.toBe(testError)
|
await expect(fetchPrice(mockToken, mockDefIndex + ";" + mockQuality)).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fetchKeyPrice uses correct parameters', async () => {
|
test('fetchKeyPrice uses correct parameters', async () => {
|
||||||
(getStorageValue as jest.Mock).mockResolvedValue(null);
|
(getStorageValue as jest.Mock).mockResolvedValue(null);
|
||||||
(priceUsingPricesTF as jest.Mock).mockResolvedValue(mockKeyPriceResponse)
|
(priceUsingPricesTF as jest.Mock).mockResolvedValue(mockKeyPriceResponse);
|
||||||
|
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockKeyPriceResponse);
|
||||||
|
|
||||||
const result = await fetchKeyPrice(mockToken)
|
const result = await fetchKeyPrice(mockToken)
|
||||||
|
|
||||||
expect(priceUsingPricesTF).toHaveBeenCalledWith(mockToken, `${defindex_key};6`)
|
|
||||||
expect(result.keys).toBe(0) // A key cannot cost a key :P
|
expect(result.keys).toBe(0) // A key cannot cost a key :P
|
||||||
expect(result.metal).toBe(mockKeyPriceResponse.metal)
|
expect(result.metal).toBe(mockKeyPriceResponse.metal)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, test, mock } from "bun:test";
|
import { describe, expect, test, mock, beforeEach, jest } from "bun:test";
|
||||||
import { ItemSchema, ItemSlot, getItemIndexByName, getTradableStatusByDefindex, getTradableStatusByName, linkBotkillerVariants, linkFestiveVariants, prepareSchema } from '../src/content/schemaService'
|
import { ItemSchema, ItemSlot, getItemIndexByName, getTradableStatusByDefindex, getTradableStatusByName, linkBotkillerVariants, linkFestiveVariants, prepareSchema } from '../src/content/schemaService'
|
||||||
|
|
||||||
// Mock the storage and log functions
|
// Mock the storage and log functions
|
||||||
@@ -13,6 +13,24 @@ mock.module('../src/content/utils/log', () => ({
|
|||||||
logError: mock(() => {})
|
logError: mock(() => {})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockSchemaResponse = [
|
||||||
|
{
|
||||||
|
defindex: 1,
|
||||||
|
item_name: 'Test Item',
|
||||||
|
item_slot: 'misc',
|
||||||
|
attributes: [
|
||||||
|
{ "name": "cannot trade", "class": "cannot_trade", "value": 1 }
|
||||||
|
],
|
||||||
|
capabilities: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defindex: 208,
|
||||||
|
item_name: 'Flame Thrower',
|
||||||
|
item_slot: 'primary',
|
||||||
|
capabilities: { can_killstreakify: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const mockSchema: ItemSchema = {
|
const mockSchema: ItemSchema = {
|
||||||
'21': {
|
'21': {
|
||||||
name: 'Flame Thrower',
|
name: 'Flame Thrower',
|
||||||
@@ -89,6 +107,7 @@ const mockSchema: ItemSchema = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Schema Service', () => {
|
describe('Schema Service', () => {
|
||||||
|
|
||||||
test('getItemIndexByName returns correct defindex', () => {
|
test('getItemIndexByName returns correct defindex', () => {
|
||||||
expect(getItemIndexByName(mockSchema, 'Flame Thrower')).toBe(208)
|
expect(getItemIndexByName(mockSchema, 'Flame Thrower')).toBe(208)
|
||||||
expect(getItemIndexByName(mockSchema, 'Mann Co. Supply Crate Key')).toBe(5021)
|
expect(getItemIndexByName(mockSchema, 'Mann Co. Supply Crate Key')).toBe(5021)
|
||||||
@@ -113,25 +132,12 @@ describe('Schema Service', () => {
|
|||||||
// Mock GM_fetch response
|
// Mock GM_fetch response
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => [
|
json: async () => mockSchemaResponse
|
||||||
{
|
|
||||||
defindex: 1,
|
|
||||||
item_name: 'Test Item',
|
|
||||||
item_slot: 'misc',
|
|
||||||
attributes: [
|
|
||||||
{ "name": "cannot trade", "class": "cannot_trade", "value": 1 }
|
|
||||||
],
|
|
||||||
capabilities: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defindex: 208,
|
|
||||||
item_name: 'Flame Thrower',
|
|
||||||
item_slot: 'primary',
|
|
||||||
capabilities: { can_killstreakify: true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock Chrome runtime message
|
||||||
|
(chrome.runtime.sendMessage as jest.Fn).mockImplementation(() => mockSchemaResponse);
|
||||||
|
|
||||||
// Mock GM_fetch
|
// Mock GM_fetch
|
||||||
globalThis.GM_fetch = mock(async () => mockResponse);
|
globalThis.GM_fetch = mock(async () => mockResponse);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
||||||
|
import { mock } from "bun:test";
|
||||||
|
|
||||||
GlobalRegistrator.register();
|
GlobalRegistrator.register();
|
||||||
|
Object.assign(global, require('jest-chrome'))
|
||||||
|
|
||||||
|
// Mock GM_fetch
|
||||||
|
globalThis.GM_fetch = mock(async () => {});
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -1,8 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "tf2wikipricing",
|
"name": "tf2wikipricing",
|
||||||
"version": "0.7.1",
|
"displayName": "TF2 Wiki Pricing",
|
||||||
|
"version": "0.8.0",
|
||||||
"description": "Adds item pricing to the Team Fortress 2 wiki",
|
"description": "Adds item pricing to the Team Fortress 2 wiki",
|
||||||
|
"author": "rapture.party",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/css": "^0.7.0",
|
||||||
|
"@eslint/js": "^9.25.1",
|
||||||
|
"@eslint/json": "^0.12.0",
|
||||||
"@happy-dom/global-registrator": "^17.4.4",
|
"@happy-dom/global-registrator": "^17.4.4",
|
||||||
"@types/firefox-webext-browser": "^120.0.4",
|
"@types/firefox-webext-browser": "^120.0.4",
|
||||||
"@types/greasemonkey": "^4.0.7",
|
"@types/greasemonkey": "^4.0.7",
|
||||||
@@ -12,10 +17,13 @@
|
|||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"bun-types": "^1.2.5",
|
"bun-types": "^1.2.5",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
|
"eslint": "^9.25.1",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"tf2-static-schema": "^1.74.0",
|
"tf2-static-schema": "^1.74.0",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
|
"typescript-eslint": "^8.31.0",
|
||||||
"webpack": "^5.94.0",
|
"webpack": "^5.94.0",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
},
|
||||||
@@ -35,12 +43,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chrome": "^0.0.270",
|
"@types/chrome": "^0.0.270",
|
||||||
"base64-inline-loader": "^2.0.1",
|
"base64-inline-loader": "^2.0.1",
|
||||||
|
"crx": "^5.0.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"css-to-string-loader": "^0.1.3",
|
"css-to-string-loader": "^0.1.3",
|
||||||
"extract-loader": "^5.1.0",
|
"extract-loader": "^5.1.0",
|
||||||
|
"jest-chrome": "^0.8.0",
|
||||||
"jsonc-loader": "^0.1.1",
|
"jsonc-loader": "^0.1.1",
|
||||||
|
"mini-css-extract-plugin": "^2.9.2",
|
||||||
|
"postcss-loader": "^8.1.1",
|
||||||
|
"postcss-url": "^10.1.3",
|
||||||
"style-loader": "^4.0.0",
|
"style-loader": "^4.0.0",
|
||||||
"to-string-loader": "^1.2.0",
|
"to-string-loader": "^1.2.0",
|
||||||
"url-loader": "^4.1.1"
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack-remove-empty-scripts": "^1.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/background/background.ts
Normal file
111
src/background/background.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
function(request, sender, sendResponse) {
|
||||||
|
if (request.contentScriptQuery == "queryExchangeRates") {
|
||||||
|
const url = "https://open.er-api.com/v6/latest/USD";
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => sendResponse(json))
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Failed to get exchange rates", error);
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
function(request, sender, sendResponse) {
|
||||||
|
if (request.contentScriptQuery == "querySchema") {
|
||||||
|
const url = "https://raw.githubusercontent.com/danocmx/node-tf2-static-schema/master/static/items.json";
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => sendResponse(json))
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Failed to get schema", error);
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
function(request, sender, sendResponse) {
|
||||||
|
if (request.contentScriptQuery == "getPricesTFToken") {
|
||||||
|
fetch('https://api2.prices.tf/auth/access', {
|
||||||
|
method: 'post',
|
||||||
|
headers: new Headers({
|
||||||
|
'Accept': 'application/json'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => sendResponse(json['accessToken']))
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Failed to get access token", error);
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class PricesResponse {
|
||||||
|
keys: number
|
||||||
|
metal: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function priceUsingPricesTF(token: string, sku: string, retries: number = 3): Promise<PricesResponse> {
|
||||||
|
const url = `https://api2.prices.tf/prices/${encodeURIComponent(sku)}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) {
|
||||||
|
const quality: number = parseInt(sku.split(';')[1], 10);
|
||||||
|
if(quality === 6) {
|
||||||
|
// Try uncraftable variant if unique weapon
|
||||||
|
return priceUsingPricesTF(token, sku + ';uncraftable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(response.status === 503) {
|
||||||
|
// Happens if we send too many requests in a short period of time
|
||||||
|
// Retry after a few seconds
|
||||||
|
if(retries >= 0) {
|
||||||
|
console.log(`Cloudflare rate limit exceeded, trying again after 1 second, ${retries} retries left`)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return priceUsingPricesTF(token, sku, retries - 1);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Cloudflare rate limit exceeded, stopping`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const prices = new PricesResponse();
|
||||||
|
prices.keys = data['sellKeys']
|
||||||
|
prices.metal = data['sellHalfScrap'] / 18.0;
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
function(request, sender, sendResponse) {
|
||||||
|
if (request.contentScriptQuery == "priceSKU") {
|
||||||
|
const sku: string = request.sku
|
||||||
|
const service: string = request.service
|
||||||
|
const token: string = request.token
|
||||||
|
if(token === "" || !token) {
|
||||||
|
sendResponse(new Error("No token provided"))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (service) {
|
||||||
|
case "prices.tf": {
|
||||||
|
priceUsingPricesTF(token, sku)
|
||||||
|
.then((response) => sendResponse({response}))
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse(error);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import styleCss from './style.css'
|
declare const __ENV_WEBEXTENSION: boolean;
|
||||||
|
declare const __ENV_USERSCRIPT: boolean;
|
||||||
|
|
||||||
import { logDebug, log, logError } from './utils/log'
|
import { logDebug, log, logError } from './utils/log'
|
||||||
import { getPricesToken, priceUsingPricesTF } from './pricing/pricestf'
|
import { getPricesToken } from './pricing/pricestf'
|
||||||
import itemQualities from 'tf2-static-schema/static/qualities.json';
|
import itemQualities from 'tf2-static-schema/static/qualities.json';
|
||||||
import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, ItemSlot, prepareSchema, wipeSchema } from './schemaService'
|
import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, ItemSlot, prepareSchema, wipeSchema } from './schemaService'
|
||||||
import { $T, extractLocaleFromURL } from './utils/localization'
|
import { $T, extractLocaleFromURL } from './utils/localization'
|
||||||
@@ -10,10 +11,10 @@ import { createPriceRow, createStoreButton } from './uiRenderer'
|
|||||||
import { findFirstElement, findFirstChildElement } from './utils/dom'
|
import { findFirstElement, findFirstChildElement } from './utils/dom'
|
||||||
import { extractPageTitleFromURL } from './utils/url';
|
import { extractPageTitleFromURL } from './utils/url';
|
||||||
import { ExchangeRates, prepareExchangeRates } from './exchangeRateService';
|
import { ExchangeRates, prepareExchangeRates } from './exchangeRateService';
|
||||||
var itemSchema: ItemSchema | null;
|
let itemSchema: ItemSchema | null;
|
||||||
var exchangeRates: ExchangeRates | null;
|
let exchangeRates: ExchangeRates | null;
|
||||||
|
|
||||||
var locale: string = 'en'
|
let locale: string = 'en'
|
||||||
|
|
||||||
/** Exclude these from the pricelist. */
|
/** Exclude these from the pricelist. */
|
||||||
const excludedQualities = new Set([
|
const excludedQualities = new Set([
|
||||||
@@ -22,8 +23,13 @@ const excludedQualities = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function getKeyByValue(object: any, value: string) {
|
function getKeyByValue<K extends string | number | symbol, V>(obj: Record<K, V>, value: V): K | undefined {
|
||||||
return Object.keys(object).find(key => object[key] === value);
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
|
if (val === value) {
|
||||||
|
return key as unknown as K;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function
|
// Main function
|
||||||
@@ -33,8 +39,8 @@ async function inject() {
|
|||||||
// Not an item page
|
// Not an item page
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var itemIndex: number | null = null;
|
let itemIndex: number | null = null;
|
||||||
var itemName: string | null = null;
|
let itemName: string | null = null;
|
||||||
|
|
||||||
// Find buy buttons
|
// Find buy buttons
|
||||||
const buyButton = findFirstChildElement('.btn_buynow', itemInfobox);
|
const buyButton = findFirstChildElement('.btn_buynow', itemInfobox);
|
||||||
@@ -52,8 +58,6 @@ async function inject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = document.URL;
|
|
||||||
|
|
||||||
if (itemName && !itemIndex) {
|
if (itemName && !itemIndex) {
|
||||||
itemIndex = getItemIndexByName(itemSchema, itemName)
|
itemIndex = getItemIndexByName(itemSchema, itemName)
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,7 @@ async function inject() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qualities: number[] = []
|
const qualities: number[] = []
|
||||||
|
|
||||||
const firstQualityTag = findFirstChildElement('.quality-tag', itemInfobox);
|
const firstQualityTag = findFirstChildElement('.quality-tag', itemInfobox);
|
||||||
|
|
||||||
@@ -122,7 +126,7 @@ async function inject() {
|
|||||||
// th.infobox-header (Basic Information)
|
// th.infobox-header (Basic Information)
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
var storeButtons: HTMLTableRowElement[] = [];
|
const storeButtons: HTMLTableRowElement[] = [];
|
||||||
|
|
||||||
// backpack.tf button
|
// backpack.tf button
|
||||||
storeButtons.push(createStoreButton("backpack.tf", new URL(`https://backpack.tf/classifieds?item=${encodeURIComponent(itemName)}`)));
|
storeButtons.push(createStoreButton("backpack.tf", new URL(`https://backpack.tf/classifieds?item=${encodeURIComponent(itemName)}`)));
|
||||||
@@ -171,7 +175,7 @@ async function inject() {
|
|||||||
priceProgressRow.appendChild(priceProgressData);
|
priceProgressRow.appendChild(priceProgressData);
|
||||||
priceInfoboxHeadingRow.insertAdjacentElement('afterend', priceProgressRow);
|
priceInfoboxHeadingRow.insertAdjacentElement('afterend', priceProgressRow);
|
||||||
|
|
||||||
var token: string | null;
|
let token: string | null;
|
||||||
|
|
||||||
// Steam Community Market
|
// Steam Community Market
|
||||||
// TODO: Change this to lazy-load, so that it doesn't make network requests when we have cached data.
|
// TODO: Change this to lazy-load, so that it doesn't make network requests when we have cached data.
|
||||||
@@ -187,7 +191,7 @@ async function inject() {
|
|||||||
log('Failed to get an access token for prices.tf: ' + err);
|
log('Failed to get an access token for prices.tf: ' + err);
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateTime: Date | null = null;
|
let updateTime: Date | null = null;
|
||||||
|
|
||||||
enum PriceRowCategory {
|
enum PriceRowCategory {
|
||||||
None,
|
None,
|
||||||
@@ -201,19 +205,19 @@ async function inject() {
|
|||||||
row: HTMLTableRowElement
|
row: HTMLTableRowElement
|
||||||
category: PriceRowCategory
|
category: PriceRowCategory
|
||||||
}
|
}
|
||||||
var priceRows: PriceRow[]= [];
|
const priceRows: PriceRow[]= [];
|
||||||
|
|
||||||
// Get current key price
|
// Get current key price
|
||||||
const keyPrice = await fetchKeyPrice(token);
|
const keyPrice = await fetchKeyPrice(token);
|
||||||
|
|
||||||
var currentTime = new Date()
|
const currentTime = new Date()
|
||||||
|
|
||||||
const promises = qualities.filter(x => !excludedQualities.has(x)).map(async (quality) => {
|
const promises = qualities.filter(x => !excludedQualities.has(x)).map(async (quality) => {
|
||||||
|
|
||||||
const qualifiedName = ((quality != 6 ? itemQualities[quality as unknown as keyof typeof itemQualities].toString() : '') + ' ' + itemName).trim()
|
const qualifiedName = ((quality != 6 ? itemQualities[quality as unknown as keyof typeof itemQualities].toString() : '') + ' ' + itemName).trim()
|
||||||
// logDebug(`Fetching price for ${qualifiedName}`)
|
logDebug(`Saving price for ${qualifiedName}`)
|
||||||
|
|
||||||
var data: ItemPriceData | null
|
let data: ItemPriceData | null
|
||||||
try {
|
try {
|
||||||
data = await fetchPrice(token, itemIndex + ";" + quality, currentTime);
|
data = await fetchPrice(token, itemIndex + ";" + quality, currentTime);
|
||||||
updateTime = new Date(data.update)
|
updateTime = new Date(data.update)
|
||||||
@@ -229,25 +233,21 @@ async function inject() {
|
|||||||
|
|
||||||
// Check item schema for Australium variant of current defindex
|
// Check item schema for Australium variant of current defindex
|
||||||
if(itemSchema[itemIndex].hasAustraliumVariant) {
|
if(itemSchema[itemIndex].hasAustraliumVariant) {
|
||||||
promises.push(new Promise(async (resolve) => {
|
promises.push(fetchPrice(token, `${itemIndex};11;australium`, currentTime).then(data => {
|
||||||
logDebug(`Fetching price for Australium ${itemName}`)
|
updateTime = new Date(data.update)
|
||||||
var data: ItemPriceData | null
|
logDebug(`Saving price for Australium ${itemName}`)
|
||||||
try {
|
|
||||||
data = await fetchPrice(token, `${itemIndex};11;australium`, currentTime);
|
|
||||||
updateTime = new Date(data.update)
|
|
||||||
} catch {
|
|
||||||
log(`Australium ${itemName} is unpriced or unavailable, skipping...`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceRow = createPriceRow($T("Australium"), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Australium_weapons")
|
const priceRow = createPriceRow($T("Australium"), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Australium_weapons")
|
||||||
|
|
||||||
priceRows.push({order: 99, row: priceRow, category: PriceRowCategory.None})
|
priceRows.push({order: 99, row: priceRow, category: PriceRowCategory.None})
|
||||||
resolve()
|
})
|
||||||
return
|
.catch((error) => {
|
||||||
|
logError(error)
|
||||||
|
log(`Australium ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
var festiveHeadingRow: HTMLTableRowElement | null
|
let festiveHeadingRow: HTMLTableRowElement | null
|
||||||
// Check item schema for Festive variant of current defindex
|
// Check item schema for Festive variant of current defindex
|
||||||
if(itemSchema[itemIndex].festiveVariant != null) {
|
if(itemSchema[itemIndex].festiveVariant != null) {
|
||||||
/// Create subheading
|
/// Create subheading
|
||||||
@@ -261,41 +261,33 @@ async function inject() {
|
|||||||
festiveHeadingRow.style.display = 'none';
|
festiveHeadingRow.style.display = 'none';
|
||||||
festiveHeadingRow.appendChild(festiveHeading);
|
festiveHeadingRow.appendChild(festiveHeading);
|
||||||
|
|
||||||
promises.push(new Promise(async (resolve) => {
|
promises.push(fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};6`, currentTime).then(data => {
|
||||||
logDebug(`Fetching price for Festive ${itemName}`)
|
updateTime = new Date(data.update)
|
||||||
var data: ItemPriceData | null
|
logDebug(`Saving price for Festive ${itemName}`)
|
||||||
try {
|
|
||||||
data = await fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};6`, currentTime);
|
|
||||||
updateTime = new Date(data.update)
|
|
||||||
} catch {
|
|
||||||
log(`Festive ${itemName} is unpriced or unavailable, skipping...`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceRow = createPriceRow($T("Unique"), data, keyPrice, exchangeRates, locale)
|
const priceRow = createPriceRow($T("Unique"), data, keyPrice, exchangeRates, locale)
|
||||||
|
|
||||||
priceRows.push({order: -1, row: priceRow, category: PriceRowCategory.Festive})
|
priceRows.push({order: -1, row: priceRow, category: PriceRowCategory.Festive})
|
||||||
resolve()
|
})
|
||||||
return
|
.catch((error) => {
|
||||||
|
logError(error)
|
||||||
|
log(`Festive ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}))
|
}))
|
||||||
promises.push(new Promise(async (resolve) => {
|
promises.push(fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};11`, currentTime).then(data => {
|
||||||
logDebug(`Fetching price for Strange Festive ${itemName}`)
|
updateTime = new Date(data.update)
|
||||||
var data: ItemPriceData | null
|
logDebug(`Saving price for Strange Festive ${itemName}`)
|
||||||
try {
|
|
||||||
data = await fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};11`, currentTime);
|
|
||||||
updateTime = new Date(data.update)
|
|
||||||
} catch {
|
|
||||||
log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceRow = createPriceRow($T("Strange"), data, keyPrice, exchangeRates, locale)
|
const priceRow = createPriceRow($T("Strange"), data, keyPrice, exchangeRates, locale)
|
||||||
|
|
||||||
priceRows.push({order: 11, row: priceRow, category: PriceRowCategory.Festive})
|
priceRows.push({order: 11, row: priceRow, category: PriceRowCategory.Festive})
|
||||||
resolve()
|
})
|
||||||
return
|
.catch((error) => {
|
||||||
|
logError(error)
|
||||||
|
log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
var killstreakKitHeadingRow: HTMLTableRowElement | null
|
let killstreakKitHeadingRow: HTMLTableRowElement | null
|
||||||
// Check for Killstreak Kits
|
// Check for Killstreak Kits
|
||||||
if(itemSchema[itemIndex].slot == ItemSlot.Primary ||
|
if(itemSchema[itemIndex].slot == ItemSlot.Primary ||
|
||||||
itemSchema[itemIndex].slot == ItemSlot.Secondary ||
|
itemSchema[itemIndex].slot == ItemSlot.Secondary ||
|
||||||
@@ -312,30 +304,23 @@ async function inject() {
|
|||||||
killstreakKitHeadingRow.style.display = 'none';
|
killstreakKitHeadingRow.style.display = 'none';
|
||||||
killstreakKitHeadingRow.appendChild(heading);
|
killstreakKitHeadingRow.appendChild(heading);
|
||||||
[1,2,3].map((tier) => {
|
[1,2,3].map((tier) => {
|
||||||
promises.push(new Promise(async (resolve) => {
|
let kitIndex: number
|
||||||
logDebug(`Fetching price for ${itemName} Killstreak Kit Tier ${tier}`)
|
switch (tier) {
|
||||||
var data: ItemPriceData | null
|
default:
|
||||||
try {
|
case 1: kitIndex = 6527; break;
|
||||||
var kitIndex: number
|
case 2: kitIndex = 6523; break;
|
||||||
switch (tier) {
|
case 3: kitIndex = 6526; break;
|
||||||
default:
|
}
|
||||||
case 1: kitIndex = 6527; break;
|
promises.push(fetchPrice(token, `${kitIndex};6;uncraftable;kt-${tier};td-${itemIndex}`, currentTime).then(data => {
|
||||||
case 2: kitIndex = 6523; break;
|
updateTime = new Date(data.update)
|
||||||
case 3: kitIndex = 6526; break;
|
logDebug(`Saving price for ${itemName} Killstreak Kit Tier ${tier}`)
|
||||||
}
|
|
||||||
data = await fetchPrice(token, `${kitIndex};6;uncraftable;kt-${tier};td-${itemIndex}`, currentTime);
|
|
||||||
updateTime = new Date(data.update)
|
|
||||||
} catch {
|
|
||||||
log(`${itemName} Killstreak Kit Tier ${tier} is unpriced or unavailable, skipping...`)
|
|
||||||
resolve()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceRow = createPriceRow($T(`kt-${tier}`), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Killstreak_Kit")
|
const priceRow = createPriceRow($T(`kt-${tier}`), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Killstreak_Kit")
|
||||||
|
|
||||||
priceRows.push({order: tier, row: priceRow, category: PriceRowCategory.KillstreakKit})
|
priceRows.push({order: tier, row: priceRow, category: PriceRowCategory.KillstreakKit})
|
||||||
resolve()
|
})
|
||||||
return
|
.catch((error) => {
|
||||||
|
logError(`Failed to fetch price for ${itemName} Killstreak Kit Tier ${tier}`, error)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -351,7 +336,7 @@ async function inject() {
|
|||||||
"Silver Mk.II",
|
"Silver Mk.II",
|
||||||
"Gold Mk.II",
|
"Gold Mk.II",
|
||||||
]
|
]
|
||||||
var botKillerHeadingRow: HTMLTableRowElement | null
|
let botKillerHeadingRow: HTMLTableRowElement | null
|
||||||
if(itemSchema[itemIndex].botkillerVariants != null && itemSchema[itemIndex].botkillerVariants.length > 0) {
|
if(itemSchema[itemIndex].botkillerVariants != null && itemSchema[itemIndex].botkillerVariants.length > 0) {
|
||||||
/// Create subheading
|
/// Create subheading
|
||||||
botKillerHeadingRow = document.createElement("tr")
|
botKillerHeadingRow = document.createElement("tr")
|
||||||
@@ -366,25 +351,18 @@ async function inject() {
|
|||||||
|
|
||||||
itemSchema[itemIndex].botkillerVariants.map((variantIndex) => {
|
itemSchema[itemIndex].botkillerVariants.map((variantIndex) => {
|
||||||
const itemName = itemSchema[variantIndex].name
|
const itemName = itemSchema[variantIndex].name
|
||||||
// FIXME: variantName should match wiki display name
|
|
||||||
const variantName = itemName.includes('Mk.II') ? itemName.split(' ')[0] + ' Mk.II' : itemName.split(' ')[0]
|
const variantName = itemName.includes('Mk.II') ? itemName.split(' ')[0] + ' Mk.II' : itemName.split(' ')[0]
|
||||||
promises.push(new Promise(async (resolve) => {
|
promises.push(fetchPrice(token, `${variantIndex};11`, currentTime).then(data => {
|
||||||
logDebug(`Fetching price for ${itemName}`)
|
logDebug(`Saving price for ${itemName}`)
|
||||||
var data: ItemPriceData | null
|
updateTime = new Date(data.update)
|
||||||
try {
|
|
||||||
data = await fetchPrice(token, `${variantIndex};11`, currentTime);
|
|
||||||
updateTime = new Date(data.update)
|
|
||||||
} catch {
|
|
||||||
log(`${itemName} is unpriced or unavailable, skipping...`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceRow = createPriceRow($T(variantName), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Botkiller_weapons")
|
const priceRow = createPriceRow($T(variantName), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Botkiller_weapons")
|
||||||
|
|
||||||
// FIXME: order should be by release
|
|
||||||
// Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II
|
|
||||||
priceRows.push({order: botkillerOrder.indexOf(variantName), row: priceRow, category: PriceRowCategory.Botkiller})
|
priceRows.push({order: botkillerOrder.indexOf(variantName), row: priceRow, category: PriceRowCategory.Botkiller})
|
||||||
resolve()
|
})
|
||||||
return
|
.catch((error) => {
|
||||||
|
logError(error)
|
||||||
|
log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -425,10 +403,12 @@ async function inject() {
|
|||||||
const label = document.createElement("td");
|
const label = document.createElement("td");
|
||||||
label.colSpan = 2;
|
label.colSpan = 2;
|
||||||
label.style.fontSize = "85%";
|
label.style.fontSize = "85%";
|
||||||
|
label.style.textAlign = "center";
|
||||||
const updateText = $T("Updated %@.", locale).replace('%@', updateTime.toLocaleString(locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }))
|
const updateText = $T("Updated %@.", locale).replace('%@', updateTime.toLocaleString(locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }))
|
||||||
const attributionText = $T("Trade prices sourced from %@. Currency conversions are approximate.", locale).replace('%@', '<a rel="nofollow" class="external text" href="https://prices.tf">prices.tf</a>');
|
const attributionHeader = $T("Acknowledgements");
|
||||||
const exchangeRateAttribution = `<a rel="nofollow" class="external text" href="https://www.exchangerate-api.com">Rates By Exchange Rate API</a>.`;
|
const pricesAttribution = `<a rel="nofollow" class="external text" href="https://prices.tf">prices.tf</a>`;
|
||||||
label.innerHTML = `${updateText}<br>${attributionText}<br>${exchangeRateAttribution}`;
|
const exchangeRateAttribution = `<a rel="nofollow" class="external text" href="https://www.exchangerate-api.com">Rates By Exchange Rate API</a>`;
|
||||||
|
label.innerHTML = `${updateText}<br><b>${attributionHeader}</b><br>${pricesAttribution}<br>${exchangeRateAttribution}`;
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
|
|
||||||
priceProgressRow.insertAdjacentElement('afterend', row);
|
priceProgressRow.insertAdjacentElement('afterend', row);
|
||||||
@@ -437,10 +417,12 @@ async function inject() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addStyles() {
|
function addStyles() {
|
||||||
const head = document.head || document.getElementsByTagName('head')[0],
|
if(__ENV_USERSCRIPT) {
|
||||||
|
const head = document.head || document.getElementsByTagName('head')[0],
|
||||||
style = document.createElement('style');
|
style = document.createElement('style');
|
||||||
head.appendChild(style);
|
head.appendChild(style);
|
||||||
style.innerHTML = styleCss;
|
style.innerHTML = require('./style.css');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareSchema()
|
prepareSchema()
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getStorageValue, setStorageValue } from './storage'
|
import { getStorageValue, setStorageValue } from './storage'
|
||||||
import { logDebug, log, logError } from './utils/log'
|
import { logDebug, log, logError } from './utils/log'
|
||||||
import { storage_exchangerates, storage_exchangerates_next, storage_exchangerates_update } from './config'
|
import { storage_exchangerates, storage_exchangerates_next, storage_exchangerates_update } from './config'
|
||||||
declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
|
import { fetchWrap } from './fetchWrap';
|
||||||
import './GM_fetch'
|
declare const __ENV_WEBEXTENSION: boolean;
|
||||||
|
declare const __ENV_USERSCRIPT: boolean;
|
||||||
|
|
||||||
export interface ExchangeRates {
|
export interface ExchangeRates {
|
||||||
[key: string]: number;
|
[key: string]: number;
|
||||||
@@ -16,8 +17,8 @@ export async function wipeExchangeRates(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareExchangeRates(): Promise<ExchangeRates> {
|
export async function prepareExchangeRates(): Promise<ExchangeRates> {
|
||||||
var needsUpdate: Boolean = false
|
let needsUpdate: boolean = false
|
||||||
var rates: ExchangeRates | null = null
|
let rates: ExchangeRates | null = null
|
||||||
|
|
||||||
rates = await getStorageValue(storage_exchangerates, null);
|
rates = await getStorageValue(storage_exchangerates, null);
|
||||||
const update = await getStorageValue(storage_exchangerates_update, null)
|
const update = await getStorageValue(storage_exchangerates_update, null)
|
||||||
@@ -26,7 +27,7 @@ export async function prepareExchangeRates(): Promise<ExchangeRates> {
|
|||||||
const lastUpdateTime = new Date(update);
|
const lastUpdateTime = new Date(update);
|
||||||
const nextUpdateTime = new Date(nextUpdate);
|
const nextUpdateTime = new Date(nextUpdate);
|
||||||
log(`Exchange rates updated at ${lastUpdateTime}`);
|
log(`Exchange rates updated at ${lastUpdateTime}`);
|
||||||
if (rates == null || Object.keys(rates).length === 0 || lastUpdateTime.getTime() > nextUpdateTime.getTime()) {
|
if (rates == null || Object.keys(rates).length === 0 || Date.now() > nextUpdateTime.getTime()) {
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -35,19 +36,35 @@ export async function prepareExchangeRates(): Promise<ExchangeRates> {
|
|||||||
|
|
||||||
if(needsUpdate) {
|
if(needsUpdate) {
|
||||||
log("Exchange rates out of Date. Rebuilding...");
|
log("Exchange rates out of Date. Rebuilding...");
|
||||||
const url = "https://open.er-api.com/v6/latest/USD"
|
let exchangeResponse: {
|
||||||
const response = await GM_fetch(url);
|
rates: ExchangeRates,
|
||||||
if (response.ok) {
|
time_next_update_utc: string
|
||||||
await setStorageValue(storage_exchangerates_update, new Date().toISOString())
|
}
|
||||||
var json = await response.json()
|
if(__ENV_USERSCRIPT) {
|
||||||
if(json != null){
|
const url = "https://open.er-api.com/v6/latest/USD"
|
||||||
rates = json['rates']
|
try {
|
||||||
await setStorageValue(storage_exchangerates, rates)
|
const response: Response = await fetchWrap(url)
|
||||||
await setStorageValue(storage_exchangerates_next, json['time_next_update_utc'])
|
if(response.ok) {
|
||||||
|
exchangeResponse = await response.json()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logDebug(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
logDebug(`Exchange rates updated at ${new Date()}`)
|
|
||||||
} else {
|
} else {
|
||||||
logError(`Failed to fetch exchange rates. Status code: ${response.status}`, response)
|
exchangeResponse = await chrome.runtime.sendMessage({contentScriptQuery: "queryExchangeRates"})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if(exchangeResponse == null) {
|
||||||
|
throw new Error("Rates are null")
|
||||||
|
}
|
||||||
|
rates = exchangeResponse.rates
|
||||||
|
await setStorageValue(storage_exchangerates_update, new Date().toISOString())
|
||||||
|
await setStorageValue(storage_exchangerates, exchangeResponse.rates)
|
||||||
|
await setStorageValue(storage_exchangerates_next, exchangeResponse.time_next_update_utc)
|
||||||
|
logDebug(`Exchange rates updated at ${new Date()}`)
|
||||||
|
} catch(e) {
|
||||||
|
logError(`Failed to store exchange rates.`, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/content/fetchWrap.ts
Normal file
10
src/content/fetchWrap.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
declare let __ENV_USERSCRIPT: boolean;
|
||||||
|
declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
|
||||||
|
|
||||||
|
export function fetchWrap(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> {
|
||||||
|
if(__ENV_USERSCRIPT) {
|
||||||
|
return GM_fetch(input, init)
|
||||||
|
} else {
|
||||||
|
return fetch(input, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { defindex_key, storage_priceprefix } from "./config"
|
import { defindex_key, storage_priceprefix } from "./config"
|
||||||
import { priceUsingPricesTF } from "./pricing/pricestf"
|
import { priceUsingPricesTF } from "./pricing/pricestf"
|
||||||
import { getStorageValue, setStorageValue } from "./storage"
|
import { getStorageValue, setStorageValue } from "./storage"
|
||||||
import { logDebug, log } from "./utils/log"
|
import { logDebug } from "./utils/log"
|
||||||
|
declare const __ENV_WEBEXTENSION: boolean;
|
||||||
|
declare const __ENV_USERSCRIPT: boolean;
|
||||||
|
|
||||||
/** Pricing data for a given TF2 item. */
|
/** Pricing data for a given TF2 item. */
|
||||||
export class ItemPriceData {
|
export class ItemPriceData {
|
||||||
@@ -40,8 +42,7 @@ export async function fetchKeyPrice(token: string) {
|
|||||||
* @param ttl Time to cache results in milliseconds. 30 minutes by default.
|
* @param ttl Time to cache results in milliseconds. 30 minutes by default.
|
||||||
*/
|
*/
|
||||||
export async function fetchPrice(token: string, sku: string, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise<ItemPriceData> {
|
export async function fetchPrice(token: string, sku: string, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise<ItemPriceData> {
|
||||||
return new Promise(async (resolve, reject) => {
|
let data: ItemPriceData | null
|
||||||
var data: ItemPriceData | null
|
|
||||||
|
|
||||||
const cached: ItemPriceData = await getStorageValue(storage_priceprefix + sku, null)
|
const cached: ItemPriceData = await getStorageValue(storage_priceprefix + sku, null)
|
||||||
if (cached != null && 'keys' in cached && 'metal' in cached && !isNaN(cached.update)) {
|
if (cached != null && 'keys' in cached && 'metal' in cached && !isNaN(cached.update)) {
|
||||||
@@ -50,8 +51,8 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
|||||||
|
|
||||||
if (!data || data.sku != sku || 'update' in data && 'ttl' in data && Date.now() > (new Date(data.update).getTime() + data.ttl)) {
|
if (!data || data.sku != sku || 'update' in data && 'ttl' in data && Date.now() > (new Date(data.update).getTime() + data.ttl)) {
|
||||||
logDebug(`Fetching price data for ${sku}`)
|
logDebug(`Fetching price data for ${sku}`)
|
||||||
if(!token) {
|
if(!token || token === '') {
|
||||||
reject(401)
|
throw new Error('No token provided')
|
||||||
}
|
}
|
||||||
data = new ItemPriceData()
|
data = new ItemPriceData()
|
||||||
data.sku = sku
|
data.sku = sku
|
||||||
@@ -59,14 +60,19 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
|||||||
data.ttl = ttl
|
data.ttl = ttl
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await priceUsingPricesTF(token, sku)
|
let response: PricesResponse
|
||||||
if (response) {
|
if(__ENV_USERSCRIPT) {
|
||||||
data.keys = response.keys
|
response = await priceUsingPricesTF(token, sku)
|
||||||
data.metal = response.metal
|
} else {
|
||||||
|
response = await chrome.runtime.sendMessage({contentScriptQuery: "priceSKU", service: "prices.tf", sku: sku, token: token});
|
||||||
}
|
}
|
||||||
|
if (!response || response instanceof Error) {
|
||||||
|
throw new Error(`Bad response: ${response}`)
|
||||||
|
}
|
||||||
|
data.keys = response.keys
|
||||||
|
data.metal = response.metal
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Received ${error} error while pricing ${sku} using prices.tf`)
|
throw new Error(`Received "${error}" error while pricing ${sku} using prices.tf`)
|
||||||
reject(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('metal' in data && 'keys' in data) {
|
if ('metal' in data && 'keys' in data) {
|
||||||
@@ -75,6 +81,5 @@ export async function fetchPrice(token: string, sku: string, update: Date = new
|
|||||||
} else {
|
} else {
|
||||||
logDebug(`Using cached price data for ${sku}`)
|
logDebug(`Using cached price data for ${sku}`)
|
||||||
}
|
}
|
||||||
resolve(data)
|
return data
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
|
import { fetchWrap } from '../fetchWrap'
|
||||||
import '../GM_fetch'
|
import { logDebug, logError } from '../utils/log'
|
||||||
import { logDebug } from '../utils/log'
|
declare const __ENV_WEBEXTENSION: boolean;
|
||||||
|
declare const __ENV_USERSCRIPT: boolean;
|
||||||
|
|
||||||
async function getPricesToken(): Promise<string> {
|
async function getPricesToken(): Promise<string> {
|
||||||
return new Promise<any>((resolve, reject) => {
|
if(__ENV_USERSCRIPT) {
|
||||||
GM_fetch('https://api2.prices.tf/auth/access', {
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
fetchWrap('https://api2.prices.tf/auth/access', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
@@ -18,6 +20,9 @@ async function getPricesToken(): Promise<string> {
|
|||||||
})
|
})
|
||||||
.then((responseData) => resolve(responseData['accessToken']))
|
.then((responseData) => resolve(responseData['accessToken']))
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
return chrome.runtime.sendMessage({contentScriptQuery: 'getPricesTFToken'})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PricesResponse {
|
class PricesResponse {
|
||||||
@@ -42,63 +47,45 @@ async function priceUsingPricesTF(token: string, sku: string, retries: number =
|
|||||||
// prices.tf
|
// prices.tf
|
||||||
// https://api2.prices.tf/prices/${sku}
|
// https://api2.prices.tf/prices/${sku}
|
||||||
// Authorization: Bearer ${token}
|
// Authorization: Bearer ${token}
|
||||||
return new Promise(async (resolve, reject) => {
|
try {
|
||||||
if (!token) {
|
const response = await fetchWrap(`https://api2.prices.tf/prices/${encodeURIComponent(sku)}`, {
|
||||||
reject(401)
|
|
||||||
}
|
|
||||||
var response = await GM_fetch(`https://api2.prices.tf/prices/${encodeURIComponent(sku)}`, {
|
|
||||||
method: 'get',
|
method: 'get',
|
||||||
headers: new Headers({
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`,
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
if (response.status === 404 && sku.includes(';')) {
|
if (response.status === 404 && sku.includes(';') && !sku.includes(';uncraftable')) {
|
||||||
const quality: number = parseInt(sku.split(';')[1], 10);
|
const quality: number = parseInt(sku.split(';')[1], 10);
|
||||||
if(quality === 6) {
|
if(quality === 6) {
|
||||||
// Try uncraftable variant if unique weapon
|
// Try uncraftable variant if unique weapon
|
||||||
response = await GM_fetch(`https://api2.prices.tf/prices/${encodeURIComponent(sku + ';uncraftable')}`, {
|
return priceUsingPricesTF(token, sku + ';uncraftable');
|
||||||
method: 'get',
|
|
||||||
headers: new Headers({
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (response.status) {
|
if(response.status === 503) {
|
||||||
case 200:
|
// Happens if we send too many requests in a short period of time
|
||||||
const json = await response.json()
|
// Retry after a few seconds
|
||||||
resolve({ keys: json['sellKeys'], metal: json['sellHalfScrap'] / 18.0 })
|
if(retries >= 0) {
|
||||||
break;
|
logDebug(`Cloudflare rate limit exceeded, trying again after 1 second, ${retries} retries left`)
|
||||||
case 404:
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
reject("Not found in prices.tf")
|
return priceUsingPricesTF(token, sku, retries - 1);
|
||||||
break;
|
} else {
|
||||||
case 429:
|
throw new Error(`Cloudflare rate limit exceeded, stopping`)
|
||||||
case 503:
|
}
|
||||||
// Happens if we send too many requests in a short period of time
|
|
||||||
// Retry after a few seconds
|
|
||||||
if(retries > 0) {
|
|
||||||
logDebug(`Cloudflare rate limit exceeded, trying again after 2 seconds, ${retries} retries left`)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
try {
|
|
||||||
const retryResult = await priceUsingPricesTF(token, sku, retries - 1);
|
|
||||||
resolve(retryResult);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject("Cloudflare rate limit exceeded, stopping")
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Something went wrong
|
|
||||||
logDebug(`Received ${response.status} error while pricing ${sku}`)
|
|
||||||
logDebug(`${JSON.stringify(response.headers)}`)
|
|
||||||
reject("Unknown error")
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
})
|
if (!response.ok) {
|
||||||
|
throw new Error(`Pricing request for ${sku} failed with status code: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const prices = new PricesResponse();
|
||||||
|
prices.keys = data['sellKeys']
|
||||||
|
prices.metal = data['sellHalfScrap'] / 18.0;
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
logError(`Failed to fetch prices from prices.tf for item ${sku}`)
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getPricesToken, priceUsingPricesTF, PricesResponse }
|
export { getPricesToken, priceUsingPricesTF, PricesResponse }
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
declare const __ENV_WEBEXTENSION: boolean;
|
||||||
|
declare const __ENV_USERSCRIPT: boolean;
|
||||||
import { getStorageValue, setStorageValue } from './storage'
|
import { getStorageValue, setStorageValue } from './storage'
|
||||||
import { logDebug, log, logError } from './utils/log'
|
import { logDebug, log, logError } from './utils/log'
|
||||||
import './config'
|
import './config'
|
||||||
declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
|
|
||||||
import './GM_fetch'
|
|
||||||
import { storage_version, storage_schema, storage_lastUpdateTime } from './config'
|
import { storage_version, storage_schema, storage_lastUpdateTime } from './config'
|
||||||
import Australiums from '../resources/australiums.json'
|
import Australiums from '../resources/australiums.json'
|
||||||
|
import { fetchWrap } from './fetchWrap'
|
||||||
const semver = require('semver')
|
const semver = require('semver')
|
||||||
|
|
||||||
export function checkAustraliumVariant(defindex: number): boolean {
|
export function checkAustraliumVariant(defindex: number): boolean {
|
||||||
@@ -14,8 +15,8 @@ export function checkAustraliumVariant(defindex: number): boolean {
|
|||||||
export declare const __VERSION__: string;
|
export declare const __VERSION__: string;
|
||||||
|
|
||||||
function isDateAfterOneDay(date1: Date, date2: Date): boolean {
|
function isDateAfterOneDay(date1: Date, date2: Date): boolean {
|
||||||
var diff = date2.getTime() - date1.getTime();
|
const diff = date2.getTime() - date1.getTime();
|
||||||
var diffDays = Math.round(diff / (1000 * 3600 * 24));
|
const diffDays = Math.round(diff / (1000 * 3600 * 24));
|
||||||
return diffDays > 1;
|
return diffDays > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +37,53 @@ export class ItemSchema {
|
|||||||
[key: string]: {
|
[key: string]: {
|
||||||
name: string,
|
name: string,
|
||||||
slot: ItemSlot,
|
slot: ItemSlot,
|
||||||
tradable: Boolean,
|
tradable: boolean,
|
||||||
hasAustraliumVariant: Boolean,
|
hasAustraliumVariant: boolean,
|
||||||
festiveVariant: number | null
|
festiveVariant: number | null
|
||||||
botkillerVariants: Array<number> | null
|
botkillerVariants: Array<number> | null
|
||||||
canKillstreakify: Boolean
|
canKillstreakify: boolean
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItemIndexByName(schema: ItemSchema, name: string, excludeStock: Boolean = true, excludeDecorated: Boolean = true) {
|
interface SchemaResponseItem {
|
||||||
|
name: string;
|
||||||
|
defindex: number;
|
||||||
|
item_class: string;
|
||||||
|
item_type_name: string;
|
||||||
|
item_name: string;
|
||||||
|
item_description: string;
|
||||||
|
proper_name: boolean;
|
||||||
|
item_slot: ItemSlot;
|
||||||
|
model_player: string;
|
||||||
|
item_quality: number;
|
||||||
|
image_inventory: string;
|
||||||
|
min_ilevel: number;
|
||||||
|
max_ilevel: number;
|
||||||
|
image_url: string;
|
||||||
|
image_url_large: string;
|
||||||
|
drop_type: string;
|
||||||
|
craft_class: string;
|
||||||
|
craft_material_type: string;
|
||||||
|
capabilities: {
|
||||||
|
decodable?: boolean,
|
||||||
|
can_be_restored?: boolean;
|
||||||
|
can_card_upgrade?: boolean;
|
||||||
|
can_consume?: boolean;
|
||||||
|
can_craft_mark?: boolean;
|
||||||
|
can_gift_wrap?: boolean;
|
||||||
|
can_killstreakify?: boolean;
|
||||||
|
can_strangify?: boolean;
|
||||||
|
paintable?: boolean;
|
||||||
|
strange_parts?: boolean;
|
||||||
|
};
|
||||||
|
attributes: Array<{
|
||||||
|
name: string;
|
||||||
|
class: string;
|
||||||
|
value: number | string; // The value can sometimes be a string, but example uses numbers
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getItemIndexByName(schema: ItemSchema, name: string, excludeStock: boolean = true, excludeDecorated: boolean = true) {
|
||||||
for (const [defindex, value] of Object.entries(schema)) {
|
for (const [defindex, value] of Object.entries(schema)) {
|
||||||
if (value['name'] == name) {
|
if (value['name'] == name) {
|
||||||
const index = parseInt(defindex)
|
const index = parseInt(defindex)
|
||||||
@@ -60,7 +99,7 @@ export function getTradableStatusByDefindex(schema: ItemSchema, defindex: number
|
|||||||
return schema[defindex.toString()].tradable
|
return schema[defindex.toString()].tradable
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTradableStatusByName(schema: ItemSchema, name: string, excludeStock: Boolean = true, excludeDecorated = true,) {
|
export function getTradableStatusByName(schema: ItemSchema, name: string, excludeStock: boolean = true, excludeDecorated = true,) {
|
||||||
for (const [defindex, value] of Object.entries(schema)) {
|
for (const [defindex, value] of Object.entries(schema)) {
|
||||||
if (value['name'] == name) {
|
if (value['name'] == name) {
|
||||||
const index = parseInt(defindex)
|
const index = parseInt(defindex)
|
||||||
@@ -119,12 +158,12 @@ export async function wipeSchema(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareSchema(): Promise<ItemSchema> {
|
export async function prepareSchema(): Promise<ItemSchema> {
|
||||||
var needsUpdate: Boolean = false
|
let needsUpdate: boolean = false
|
||||||
var itemSchema: ItemSchema | null = null
|
let itemSchema: ItemSchema | null = null
|
||||||
|
|
||||||
const storedVersion: string | null = await getStorageValue(storage_version, null)
|
const storedVersion: string | null = await getStorageValue(storage_version, null)
|
||||||
if(!storedVersion || !semver.valid(storedVersion)) {
|
if(!storedVersion || !semver.valid(storedVersion)) {
|
||||||
log(`Cache is from an unknown version of the extension. Updating for version ${__VERSION__}`);
|
log(`Preparing the extension for the first time.`);
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
} else if(semver.valid(storedVersion) && semver.lt(storedVersion, __VERSION__)) {
|
} else if(semver.valid(storedVersion) && semver.lt(storedVersion, __VERSION__)) {
|
||||||
log(`Cache is from a previous version (${storedVersion}) of the extension. Updating for version ${__VERSION__}`);
|
log(`Cache is from a previous version (${storedVersion}) of the extension. Updating for version ${__VERSION__}`);
|
||||||
@@ -145,31 +184,38 @@ export async function prepareSchema(): Promise<ItemSchema> {
|
|||||||
|
|
||||||
if(needsUpdate) {
|
if(needsUpdate) {
|
||||||
log("Item Schema out of Date. Rebuilding...");
|
log("Item Schema out of Date. Rebuilding...");
|
||||||
const url = "https://raw.githubusercontent.com/danocmx/node-tf2-static-schema/master/static/items.json"
|
try {
|
||||||
const response = await GM_fetch(url);
|
|
||||||
if (response.ok) {
|
|
||||||
await setStorageValue(storage_lastUpdateTime, new Date().getTime());
|
await setStorageValue(storage_lastUpdateTime, new Date().getTime());
|
||||||
|
|
||||||
var cacheItems = {}
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const cacheItems: any = {}
|
||||||
|
|
||||||
var responseItems: any[] = await response.json()
|
let responseItems: SchemaResponseItem[];
|
||||||
|
if(__ENV_USERSCRIPT) {
|
||||||
|
const url = "https://raw.githubusercontent.com/danocmx/node-tf2-static-schema/master/static/items.json"
|
||||||
|
const response = await fetchWrap(url);
|
||||||
|
if(response.ok) {
|
||||||
|
responseItems = await response.json();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
responseItems = await chrome.runtime.sendMessage({contentScriptQuery: "querySchema"})
|
||||||
|
}
|
||||||
// We want to keep the keys `defindex`, `item_name`, and `attributes`
|
// We want to keep the keys `defindex`, `item_name`, and `attributes`
|
||||||
responseItems.forEach((item: any) => {
|
responseItems.forEach((item: SchemaResponseItem) => {
|
||||||
const defindex: number = item['defindex']
|
const defindex: number = item['defindex']
|
||||||
|
|
||||||
var tradable: Boolean = true
|
let tradable: boolean = true
|
||||||
try {
|
try {
|
||||||
if(item['attributes'] != null) {
|
if(item['attributes'] != null) {
|
||||||
if(item['attributes'].find((attribute: {}) => (attribute as any)['class'] == "cannot_trade")) {
|
if(item['attributes'].find((attribute) => attribute['class'] == "cannot_trade")) {
|
||||||
tradable = false
|
tradable = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logError(error)
|
logError(error)
|
||||||
log(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var canKillstreakify: Boolean = false
|
let canKillstreakify: boolean = false
|
||||||
try {
|
try {
|
||||||
if(item['capabilities'] != null) {
|
if(item['capabilities'] != null) {
|
||||||
if(item['capabilities']['can_killstreakify'] != null && item['capabilities']['can_killstreakify'] == true) {
|
if(item['capabilities']['can_killstreakify'] != null && item['capabilities']['can_killstreakify'] == true) {
|
||||||
@@ -178,10 +224,9 @@ export async function prepareSchema(): Promise<ItemSchema> {
|
|||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logError(error)
|
logError(error)
|
||||||
log(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(cacheItems as any)[defindex.toString()] = {
|
cacheItems[defindex.toString()] = {
|
||||||
"name": item['item_name'],
|
"name": item['item_name'],
|
||||||
"slot": item['item_slot'],
|
"slot": item['item_slot'],
|
||||||
"tradable": tradable,
|
"tradable": tradable,
|
||||||
@@ -197,8 +242,8 @@ export async function prepareSchema(): Promise<ItemSchema> {
|
|||||||
itemSchema = cacheItems
|
itemSchema = cacheItems
|
||||||
await setStorageValue(storage_version, __VERSION__);
|
await setStorageValue(storage_version, __VERSION__);
|
||||||
logDebug(`Item schema updated at ${new Date()}`)
|
logDebug(`Item schema updated at ${new Date()}`)
|
||||||
} else {
|
} catch (e) {
|
||||||
logError("Could not fetch item schema.");
|
logError("Could not fetch item schema.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return itemSchema
|
return itemSchema
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
declare var __ENV_USERSCRIPT: boolean;
|
declare let __ENV_USERSCRIPT: boolean;
|
||||||
declare var __ENV_WEBEXTENSION: boolean;
|
declare let __ENV_WEBEXTENSION: boolean;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function getStorageValue(name: string, defaultValue: string): Promise<any> {
|
function getStorageValue(name: string, defaultValue: string): Promise<any> {
|
||||||
if(__ENV_USERSCRIPT) {
|
if(__ENV_USERSCRIPT) {
|
||||||
return GM.getValue(name, defaultValue);
|
return GM.getValue(name, defaultValue);
|
||||||
} else if(__ENV_WEBEXTENSION) {
|
} else if(__ENV_WEBEXTENSION) {
|
||||||
return browser.storage.local.get(name);
|
return chrome.storage.local.get(name)
|
||||||
|
.then((result) => result[name])
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return new Promise<any>((resolve) => {
|
return new Promise<any>((resolve) => {
|
||||||
resolve(defaultValue);
|
resolve(defaultValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStorageValue(name: string, value: any): Promise<any> {
|
function setStorageValue(name: string, value: unknown): Promise<void> {
|
||||||
if(__ENV_USERSCRIPT) {
|
if(__ENV_USERSCRIPT) {
|
||||||
return GM.setValue(name, value);
|
return GM.setValue(name, value as GM.Value);
|
||||||
} else if(__ENV_WEBEXTENSION) {
|
} else if(__ENV_WEBEXTENSION) {
|
||||||
return browser.storage.local.set({name, value});
|
return chrome.storage.local.set({[name]: value});
|
||||||
} else {
|
} else {
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<void>((_, reject) => {
|
||||||
reject();
|
reject();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ export function createPriceRow(qualityName: string, data: ItemPriceData, keyPric
|
|||||||
|
|
||||||
const priceData = document.createElement("td");
|
const priceData = document.createElement("td");
|
||||||
const priceLink = document.createElement("span");
|
const priceLink = document.createElement("span");
|
||||||
var priceString: string = ''
|
let priceString: string = ''
|
||||||
|
|
||||||
if(data) {
|
if(data) {
|
||||||
const gamePrice = formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim()
|
const gamePrice = formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim()
|
||||||
const realPriceUSD = convertTF2PriceToUSD(data.keys, data.metal, keyPrice.metal)
|
const realPriceUSD = convertTF2PriceToUSD(data.keys, data.metal, keyPrice.metal)
|
||||||
|
|
||||||
const USDFormatter = new Intl.NumberFormat(locale, { style: "currency", currency: 'USD' })
|
const USDFormatter = new Intl.NumberFormat(locale, { style: "currency", currency: 'USD' })
|
||||||
var realPriceString = USDFormatter.format(realPriceUSD)
|
let realPriceString = USDFormatter.format(realPriceUSD)
|
||||||
const currency = defaultCurrencyForPageLocale(locale) ?? 'USD'
|
const currency = defaultCurrencyForPageLocale(locale) ?? 'USD'
|
||||||
if(currency !== 'USD') {
|
if(currency !== 'USD') {
|
||||||
try {
|
try {
|
||||||
@@ -56,7 +56,7 @@ export function createPriceRow(qualityName: string, data: ItemPriceData, keyPric
|
|||||||
|
|
||||||
export function createStoreButton(storeName: string, url: URL) {
|
export function createStoreButton(storeName: string, url: URL) {
|
||||||
const button = document.createElement("tr")
|
const button = document.createElement("tr")
|
||||||
var source = `<td colspan="2" class="infobox-data" style="text-align:center"><div class="plainlinks btn_wrapper" style="width:100%"><a rel="nofollow" class="external text" href="{link}" target="_blank"><span class="btn_buynow_addon_${storeName.replaceAll('.', '')}">{title}<span></span></span></a></div></td>`
|
let source = `<td colspan="2" class="infobox-data" style="text-align:center"><div class="plainlinks btn_wrapper" style="width:100%"><a rel="nofollow" class="external text" href="{link}" target="_blank"><span class="btn_buynow_addon_${storeName.replaceAll('.', '')}">{title}<span></span></span></a></div></td>`
|
||||||
source = source.replace("{link}", url.toString())
|
source = source.replace("{link}", url.toString())
|
||||||
source = source.replace("{title}", $T("View listings on %@").replace('%@', storeName))
|
source = source.replace("{title}", $T("View listings on %@").replace('%@', storeName))
|
||||||
button.innerHTML = source
|
button.innerHTML = source
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { conversion_ref_usd } from '../config';
|
|
||||||
import { $T } from './localization'
|
import { $T } from './localization'
|
||||||
|
|
||||||
function toFixed(num: number, fixed: number) {
|
function toFixed(num: number, fixed: number) {
|
||||||
var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?');
|
const re = new RegExp('^-?\\d+(?:.\\d{0,' + (fixed || -1) + '})?');
|
||||||
return num.toString().match(re)[0];
|
return num.toString().match(re)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') {
|
export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') {
|
||||||
const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2)
|
const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2)
|
||||||
|
|
||||||
var output: string = ''
|
let output: string = ''
|
||||||
if(keys > 0) {
|
if(keys > 0) {
|
||||||
output += (formattedKeys == 1.0 ? $T("%@ key") : $T("%@ keys")).replace('%@', formattedKeys.toLocaleString(locale))
|
output += (formattedKeys == 1.0 ? $T("%@ key") : $T("%@ keys")).replace('%@', formattedKeys.toLocaleString(locale))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,36 +1,45 @@
|
|||||||
const localizations: {[lang: string]: any} = {
|
import { logDebug } from "./log";
|
||||||
|
|
||||||
|
const localizations: Record<string, object> = {
|
||||||
'en': require('../../strings/en'), // English
|
'en': require('../../strings/en'), // English
|
||||||
'es': require('../../strings/es'), // Spanish
|
'es': require('../../strings/es'), // Spanish
|
||||||
// 'ja': require('../../strings/ja'), // Japanese
|
'ja': require('../../strings/ja'), // Japanese
|
||||||
// 'it': require('../../strings/it'), // Italian
|
'it': require('../../strings/it'), // Italian
|
||||||
// 'ar': require('../../strings/ar.json') as object, // Arabic
|
'ar': require('../../strings/ar'), // Arabic
|
||||||
// 'cs': require('../../strings/cs.json') as object, // Czech
|
'cs': require('../../strings/cs'), // Czech
|
||||||
// 'da': require('../../strings/da.json') as object, // Danish
|
'da': require('../../strings/da'), // Danish
|
||||||
// 'de': require('../../strings/de.json') as object, // German
|
'de': require('../../strings/de'), // German
|
||||||
// 'fi': require('../../strings/fi.json') as object, // Finnish
|
'fi': require('../../strings/fi'), // Finnish
|
||||||
// 'fr': require('../../strings/fr.json') as object, // French
|
'fr': require('../../strings/fr'), // French
|
||||||
// 'hu': require('../../strings/hu.json') as object, // Hungarian
|
'hu': require('../../strings/hu'), // Hungarian
|
||||||
// 'ko': require('../../strings/ko.json') as object, // Korean
|
'ko': require('../../strings/ko'), // Korean
|
||||||
// 'nl': require('../../strings/nl.json') as object, // Dutch
|
'nl': require('../../strings/nl'), // Dutch
|
||||||
// 'no': require('../../strings/no.json') as object, // Norwegian Bokmål
|
'no': require('../../strings/no'), // Norwegian Bokmål
|
||||||
// 'pl': require('../../strings/pl.json') as object, // Polish
|
'pl': require('../../strings/pl'), // Polish
|
||||||
// 'pt': require('../../strings/pt.json') as object, // Portuguese
|
'pt': require('../../strings/pt'), // Portuguese
|
||||||
// 'pt-BR': require('../../strings/pt-BR.json') as object, // Brazilian Portuguese
|
'pt-BR': require('../../strings/pt-BR'), // Brazilian Portuguese
|
||||||
// 'ro': require('../../strings/ro.json') as object, // Romanian
|
'ro': require('../../strings/ro'), // Romanian
|
||||||
// 'ru': require('../../strings/ru.json') as object, // Russian
|
'ru': require('../../strings/ru'), // Russian
|
||||||
// 'sv': require('../../strings/sv.json') as object, // Swedish
|
'sv': require('../../strings/sv'), // Swedish
|
||||||
// 'tr': require('../../strings/tr.json') as object, // Turkish
|
'tr': require('../../strings/tr'), // Turkish
|
||||||
// 'zh-Hans': require('../../strings/zh-Hans.json') as object, // Simplified Chinese
|
'zh-Hans': require('../../strings/zh-Hans'), // Simplified Chinese
|
||||||
// 'zh-Hant': require('../../strings/zh-Hant.json') as object, // Traditional Chinese
|
'zh-Hant': require('../../strings/zh-Hant'), // Traditional Chinese
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $T(s: string, locale?: Intl.LocalesArgument): string {
|
export function $T(s: string, locale?: Intl.LocalesArgument): string {
|
||||||
const code = locale ? locale.toString() : extractLocaleFromURL(document.URL)
|
const code = locale ? locale.toString() : extractLocaleFromURL(document.URL)
|
||||||
return localizations.hasOwnProperty(code) ? (localizations[code as unknown as keyof object])[s] || s : s;
|
if (code in localizations) {
|
||||||
|
const translation = localizations[code] as Record<string, string>;
|
||||||
|
const result = translation[s] ?? s;
|
||||||
|
logDebug(`Translating "${s}" to locale "${code}": ${result}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
logDebug(`Untranslated string "${s}" in locale "${code}`);
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractLocaleFromURL(url: string): string {
|
export function extractLocaleFromURL(url: string): string {
|
||||||
var split = url.substring(url.indexOf("/wiki/") + "/wiki/".length);
|
const split = url.substring(url.indexOf("/wiki/") + "/wiki/".length);
|
||||||
if (split.indexOf('/') != -1) {
|
if (split.indexOf('/') != -1) {
|
||||||
// Remove language suffix e.g. `/es`
|
// Remove language suffix e.g. `/es`
|
||||||
return split.substring(split.indexOf('/') + 1);
|
return split.substring(split.indexOf('/') + 1);
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
declare var __PRODUCTION: boolean;
|
declare let __EXTENSION_NAME: string;
|
||||||
declare var __EXTENSION_NAME: string;
|
|
||||||
const logHeader = `[${__EXTENSION_NAME}] `;
|
const logHeader = `[${__EXTENSION_NAME}] `;
|
||||||
|
|
||||||
/** `console.debug` with header; automatically NO-OP on production build */
|
/** `console.debug` with header; automatically NO-OP on production build */
|
||||||
function logDebug(message?: any, ...optionalParams: any[]): void {
|
function logDebug(message?: string, ...optionalParams: Array<unknown>): void {
|
||||||
if(process.env.NODE_ENV !== 'production') console.debug(logHeader + message, optionalParams);
|
if(process.env.NODE_ENV !== 'production') console.debug(logHeader + message, optionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `console.log` with header */
|
/** `console.log` with header */
|
||||||
function log(message?: any, ...optionalParams: any[]): void {
|
function log(message?: string, ...optionalParams: Array<unknown>): void {
|
||||||
console.log(logHeader + message, optionalParams)
|
console.log(logHeader + message, optionalParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `console.error` with header */
|
/** `console.error` with header */
|
||||||
function logError(message?: any, ...optionalParams: any[]): void {
|
function logError(message?: string, ...optionalParams: Array<unknown>): void {
|
||||||
console.error(logHeader + message, optionalParams)
|
console.error(logHeader + message, optionalParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export function extractPageTitleFromURL(url: string): string {
|
export function extractPageTitleFromURL(url: string): string {
|
||||||
var split = url.substring(url.indexOf("/wiki/") + "/wiki/".length);
|
let split = url.substring(url.indexOf("/wiki/") + "/wiki/".length);
|
||||||
if (split.indexOf('/') != -1) {
|
if (split.indexOf('/') != -1) {
|
||||||
// Remove language suffix (/es)
|
// Remove language suffix (/es)
|
||||||
split = split.substring(0, split.indexOf('/'));
|
split = split.substring(0, split.indexOf('/'));
|
||||||
|
|||||||
27
src/eslint.config.mjs
Normal file
27
src/eslint.config.mjs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import css from "@eslint/css";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{ ignores: ["**/GM_fetch/**/*.js"] },
|
||||||
|
{ files: ["**/*.{js,mjs,cjs,ts}"], plugins: { js }, extends: ["js/recommended"] },
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-require-imports": "off"
|
||||||
|
},
|
||||||
|
languageOptions: { globals: globals.browser }
|
||||||
|
},
|
||||||
|
{ files: ["**/strings/*.js"], plugins: { js }, extends: ["js/recommended"] },
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-require-imports": "off"
|
||||||
|
},
|
||||||
|
languageOptions: { sourceType: "commonjs" }
|
||||||
|
},
|
||||||
|
{ files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] },
|
||||||
|
]);
|
||||||
@@ -4,14 +4,34 @@
|
|||||||
"author": EXTENSION_AUTHOR,
|
"author": EXTENSION_AUTHOR,
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"version": EXTENSION_VERSION,
|
"version": EXTENSION_VERSION,
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"scripting"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://wiki.teamfortress.com/wiki/*",
|
||||||
|
"https://*.prices.tf/*",
|
||||||
|
"https://open.er-api.com/*"
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["lib/style.css", "resources/*"],
|
||||||
|
"matches": ["https://wiki.teamfortress.com/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["*://wiki.teamfortress.com/wiki/*"],
|
"matches": ["*://wiki.teamfortress.com/wiki/*"],
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
|
"css": ["lib/style.css"],
|
||||||
"js": ["content/content.js"]
|
"js": ["content/content.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background/background.js",
|
||||||
|
"type": "module"
|
||||||
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "icons/icon-48.png",
|
"48": "icons/icon-48.png",
|
||||||
"96": "icons/icon-96.png"
|
"96": "icons/icon-96.png"
|
||||||
|
|||||||
44
src/strings/ar.js
Normal file
44
src/strings/ar.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/cs.js
Normal file
44
src/strings/cs.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/da.js
Normal file
44
src/strings/da.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/de.js
Normal file
44
src/strings/de.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ module.exports = {
|
|||||||
"Community Pricing": "Community Pricing",
|
"Community Pricing": "Community Pricing",
|
||||||
// Itembox footer
|
// Itembox footer
|
||||||
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
"Trade prices sourced from %@. Currency conversions are approximate.": "Trade prices sourced from %@. Currency conversions are approximate.", // %@ is always a URL, (eg. prices.tf)
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
// Price strings
|
// Price strings
|
||||||
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ module.exports = {
|
|||||||
"Community Pricing": "Precios de la comunidad",
|
"Community Pricing": "Precios de la comunidad",
|
||||||
// Itembox footer
|
// Itembox footer
|
||||||
"Updated %@.": "Actualizado %@.", // %@ is a date string, sourced from AppleGlot
|
"Updated %@.": "Actualizado %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
"Trade prices sourced from %@. Currency conversions are approximate.": "Precios comerciales obtenidos de %@. Las conversiones de divisas son aproximadas.", // %@ is always a URL, (eg. prices.tf)
|
"Acknowledgements": "Agradecimientos", // sourced from AppleGlot
|
||||||
|
|
||||||
// Price strings
|
// Price strings
|
||||||
"Data unavailable": "Datos no disponibles", // sourced from AppleGlot
|
"Data unavailable": "Datos no disponibles", // sourced from AppleGlot
|
||||||
"%@ ref": "%@ ref",
|
"%@ ref": "%@ ref",
|
||||||
"%@ key": "%@ llave",
|
"%@ key": "%@ llave",
|
||||||
"%@ keys": "%@ llaves",
|
"%@ keys": "%@ llaves",
|
||||||
|
|
||||||
// Item quality names, all sourced from TF2 wiki
|
// Item quality names, all sourced from TF2 wiki
|
||||||
"Normal": "de Calidad Normal",
|
"Normal": "de Calidad Normal",
|
||||||
"Genuine": "de Calidad Genuina",
|
"Genuine": "de Calidad Genuina",
|
||||||
@@ -38,7 +38,7 @@ module.exports = {
|
|||||||
|
|
||||||
// Killstreak tiers sourced from TF2 wiki
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
"Killstreak Kit": "Kit Cuentarrachas",
|
"Killstreak Kit": "Kit Cuentarrachas",
|
||||||
"kt-1": "Standard",
|
"kt-1": "Estándar",
|
||||||
"kt-2": "Especializado",
|
"kt-2": "Especializado",
|
||||||
"kt-3": "Profesional",
|
"kt-3": "Profesional",
|
||||||
}
|
}
|
||||||
44
src/strings/fi.js
Normal file
44
src/strings/fi.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/fr.js
Normal file
44
src/strings/fr.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/hu.js
Normal file
44
src/strings/hu.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
@@ -3,16 +3,16 @@ module.exports = {
|
|||||||
"View listings on %@": "Voir les offres sur %@",
|
"View listings on %@": "Voir les offres sur %@",
|
||||||
|
|
||||||
// Itembox header
|
// Itembox header
|
||||||
"Community Pricing": "Community Pricing",
|
"Community Pricing": "Prezzo Comunitario",
|
||||||
// Itembox footer
|
// Itembox footer
|
||||||
"Updated %@": "Updated %@.",
|
"Updated %@.": "Aggiornato il %@.",
|
||||||
"Trade prices sourced from %@. Currency conversions are approximate.": "Trade prices sourced from %@. Currency conversions are approximate.", // %@ is always a URL, (eg. prices.tf)
|
"Acknowledgements": "Note legali", // sourced from AppleGlot
|
||||||
|
|
||||||
// Price strings
|
// Price strings
|
||||||
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
"%@ ref": "%@ ref",
|
"%@ ref": "%@ raf",
|
||||||
"%@ key": "%@ key",
|
"%@ key": "%@ chiave",
|
||||||
"%@ keys": "%@ keys",
|
"%@ keys": "%@ chiavi",
|
||||||
|
|
||||||
// Item quality names, all sourced from TF2 wiki
|
// Item quality names, all sourced from TF2 wiki
|
||||||
"Normal": "Normale",
|
"Normal": "Normale",
|
||||||
@@ -22,5 +22,23 @@ module.exports = {
|
|||||||
"Strange": "Strano",
|
"Strange": "Strano",
|
||||||
"Collector's": "Da collezione",
|
"Collector's": "Da collezione",
|
||||||
"Haunted": "Stregato",
|
"Haunted": "Stregato",
|
||||||
"Australium": "Australium"
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festivo",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Ammazzabot",
|
||||||
|
"Silver": "Argentato",
|
||||||
|
"Gold": "Dorato",
|
||||||
|
"Rust": "Arrugginito",
|
||||||
|
"Blood": "Insanguinato",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamante",
|
||||||
|
"Silver Mk.II": "Argentato Mk.II",
|
||||||
|
"Gold Mk.II": "Dorato Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Kit per Serie omicide",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specializzati",
|
||||||
|
"kt-3": "Professionali",
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,10 @@ module.exports = {
|
|||||||
"View listings on %@": "%@で検索結果を見る",
|
"View listings on %@": "%@で検索結果を見る",
|
||||||
|
|
||||||
// Itembox header
|
// Itembox header
|
||||||
"Community Pricing": "Community Pricing",
|
"Community Pricing": "共同体価格",
|
||||||
// Itembox footer
|
// Itembox footer
|
||||||
"Updated %@.": "アップデート: %@。", // %@ is a date string, sourced from AppleGlot
|
"Updated %@.": "アップデート: %@。", // %@ is a date string, sourced from AppleGlot
|
||||||
"Trade prices sourced from %@. Currency conversions are approximate.": "Trade prices sourced from %@. Currency conversions are approximate.", // %@ is always a URL, (eg. prices.tf)
|
"Acknowledgements": "謝辞", // sourced from AppleGlot
|
||||||
|
|
||||||
// Price strings
|
// Price strings
|
||||||
"Data unavailable": "データがありません", // sourced from AppleGlot
|
"Data unavailable": "データがありません", // sourced from AppleGlot
|
||||||
@@ -22,5 +22,23 @@ module.exports = {
|
|||||||
"Strange": "ストレンジ",
|
"Strange": "ストレンジ",
|
||||||
"Collector's": "Collector's",
|
"Collector's": "Collector's",
|
||||||
"Haunted": "Haunted",
|
"Haunted": "Haunted",
|
||||||
"Australium": "オーストラリウム"
|
"Australium": "オーストラリウム",
|
||||||
|
"Festive": "フェスティブ",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "ボットキラー",
|
||||||
|
"Silver": "シルバー",
|
||||||
|
"Gold": "ゴールド",
|
||||||
|
"Rust": "さびた",
|
||||||
|
"Blood": "ブラッド",
|
||||||
|
"Carbonado": "黒ダイヤ",
|
||||||
|
"Diamond": "ダイヤモンド",
|
||||||
|
"Silver Mk.II": "シルバー Mk.II",
|
||||||
|
"Gold Mk.II": "ゴールド Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "キルストリークキット",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
}
|
}
|
||||||
44
src/strings/ko.js
Normal file
44
src/strings/ko.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/nl.js
Normal file
44
src/strings/nl.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/no.js
Normal file
44
src/strings/no.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/pl.js
Normal file
44
src/strings/pl.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/pt-BR.js
Normal file
44
src/strings/pt-BR.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/pt.js
Normal file
44
src/strings/pt.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/ro.js
Normal file
44
src/strings/ro.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/ru.js
Normal file
44
src/strings/ru.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "Просмотреть объявления на %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Цены сообщества",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Обновлено %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Уведомления", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Данные недоступны", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ реф",
|
||||||
|
"%@ key": "%@ ключ",
|
||||||
|
"%@ keys": "%@ ключей",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Обычное",
|
||||||
|
"Genuine": "высшей пробы",
|
||||||
|
"Vintage": "старой закалки",
|
||||||
|
"Unique": "Уникальный",
|
||||||
|
"Strange": "странного типа",
|
||||||
|
"Collector's": "из коллекции",
|
||||||
|
"Haunted": "призрачного вида",
|
||||||
|
"Australium": "из австралия",
|
||||||
|
"Festive": "Праздничный",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Боткиллер",
|
||||||
|
"Silver": "Серебряный",
|
||||||
|
"Gold": "Золотой",
|
||||||
|
"Rust": "Ржавый",
|
||||||
|
"Blood": "Кровавый",
|
||||||
|
"Carbonado": "Карбонадо",
|
||||||
|
"Diamond": "Алмазный",
|
||||||
|
"Silver Mk.II": "Серебряный вер. 2.0",
|
||||||
|
"Gold Mk.II": "Золотой вер. 2.0",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Набор убийцы",
|
||||||
|
"kt-1": "серийного",
|
||||||
|
"kt-2": "особо опасного",
|
||||||
|
"kt-3": "профессионального",
|
||||||
|
}
|
||||||
44
src/strings/sv.js
Normal file
44
src/strings/sv.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/tr.js
Normal file
44
src/strings/tr.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/zh-Hans.js
Normal file
44
src/strings/zh-Hans.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
44
src/strings/zh-Hant.js
Normal file
44
src/strings/zh-Hant.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Generic button text, %@ is always a URL (eg. backpack.tf)
|
||||||
|
"View listings on %@": "View listings on %@",
|
||||||
|
|
||||||
|
// Itembox header
|
||||||
|
"Community Pricing": "Community Pricing",
|
||||||
|
// Itembox footer
|
||||||
|
"Updated %@.": "Updated %@.", // %@ is a date string, sourced from AppleGlot
|
||||||
|
"Acknowledgements": "Acknowledgements", // sourced from AppleGlot
|
||||||
|
|
||||||
|
// Price strings
|
||||||
|
"Data unavailable": "Data unavailable", // sourced from AppleGlot
|
||||||
|
"%@ ref": "%@ ref",
|
||||||
|
"%@ key": "%@ key",
|
||||||
|
"%@ keys": "%@ keys",
|
||||||
|
|
||||||
|
// Item quality names, all sourced from TF2 wiki
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Genuine": "Genuine",
|
||||||
|
"Vintage": "Vintage",
|
||||||
|
"Unique": "Unique",
|
||||||
|
"Strange": "Strange",
|
||||||
|
"Collector's": "Collector's",
|
||||||
|
"Haunted": "Haunted",
|
||||||
|
"Australium": "Australium",
|
||||||
|
"Festive": "Festive",
|
||||||
|
|
||||||
|
// Botkiller names, all sourced from TF2 wiki
|
||||||
|
"Botkiller": "Botkiller",
|
||||||
|
"Silver": "Silver",
|
||||||
|
"Gold": "Gold",
|
||||||
|
"Rust": "Rust",
|
||||||
|
"Blood": "Blood",
|
||||||
|
"Carbonado": "Carbonado",
|
||||||
|
"Diamond": "Diamond",
|
||||||
|
"Silver Mk.II": "Silver Mk.II",
|
||||||
|
"Gold Mk.II": "Gold Mk.II",
|
||||||
|
|
||||||
|
// Killstreak tiers sourced from TF2 wiki
|
||||||
|
"Killstreak Kit": "Killstreak Kit",
|
||||||
|
"kt-1": "Standard",
|
||||||
|
"kt-2": "Specialized",
|
||||||
|
"kt-3": "Professional",
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// @name EXTENSION_NAME
|
// @name EXTENSION_NAME
|
||||||
// @description EXTENSION_DESCRIPTION
|
// @description EXTENSION_DESCRIPTION
|
||||||
// @version EXTENSION_VERSION
|
// @version EXTENSION_VERSION
|
||||||
|
// @author EXTENSION_AUTHOR
|
||||||
// @match *://wiki.teamfortress.com/wiki/*
|
// @match *://wiki.teamfortress.com/wiki/*
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @inject-into content
|
// @inject-into content
|
||||||
@@ -9,6 +10,8 @@
|
|||||||
// @domain steamcommunity.com
|
// @domain steamcommunity.com
|
||||||
// @connect prices.tf
|
// @connect prices.tf
|
||||||
// @domain prices.tf
|
// @domain prices.tf
|
||||||
|
// @connect open.er-api.com
|
||||||
|
// @domain open.er-api.com
|
||||||
// @grant GM.setValue
|
// @grant GM.setValue
|
||||||
// @grant GM_setValue
|
// @grant GM_setValue
|
||||||
// @grant GM.getValue
|
// @grant GM.getValue
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"types": ["bun-types", "jest", "greasemonkey", "firefox-webext-browser"]
|
"types": ["bun-types", "jest", "greasemonkey", "chrome", "firefox-webext-browser"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
var path = require('path');
|
var path = require('path');
|
||||||
var CopyPlugin = require('copy-webpack-plugin');
|
var CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
|
||||||
var webpack = require('webpack');
|
var webpack = require('webpack');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var package = require('./package.json');
|
var package = require('./package.json');
|
||||||
@@ -12,7 +14,7 @@ function allReplace(str, obj, quote = true) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
EXTENSION_NAME: package.name,
|
EXTENSION_NAME: package.displayName,
|
||||||
__EXTENSION_NAME: JSON.stringify(package.name),
|
__EXTENSION_NAME: JSON.stringify(package.name),
|
||||||
EXTENSION_AUTHOR: package.author,
|
EXTENSION_AUTHOR: package.author,
|
||||||
EXTENSION_DESCRIPTION: package.description,
|
EXTENSION_DESCRIPTION: package.description,
|
||||||
@@ -21,32 +23,65 @@ const defines = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
/*
|
|
||||||
// WebExtension
|
// WebExtension
|
||||||
{
|
{
|
||||||
entry: {
|
entry: {
|
||||||
content: './src/content/content.ts'
|
content: './src/content/content.ts',
|
||||||
|
background: './src/background/background.ts',
|
||||||
|
style: './src/content/style.css'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: 'ts-loader',
|
use: 'ts-loader',
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules|GM_fetch/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpg|gif|svg)$/i,
|
test: /\.css$/i,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'url-loader',
|
loader: MiniCssExtractPlugin.loader,
|
||||||
options: {
|
options: {
|
||||||
limit: true,
|
publicPath: '/', // Adjust if needed for relative path resolution
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
url: true, // Ensures url() in CSS is processed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: {
|
||||||
|
'postcss-url': {
|
||||||
|
url: (asset) => {
|
||||||
|
// Transform relative URLs to extension-style URLs
|
||||||
|
const relativePath = asset.url.replace(/^\.\.\//, '') // Remove leading ../resources part
|
||||||
|
return `chrome-extension://__MSG_@@extension_id__/${relativePath}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(jpe?g|png|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
generator: {
|
||||||
|
filename: 'resources/[name][ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
externals: {
|
||||||
|
'./src/content/GM_fetch': 'commonjs2 null'
|
||||||
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: true
|
minimize: true
|
||||||
},
|
},
|
||||||
@@ -55,12 +90,13 @@ module.exports = [
|
|||||||
filename: "[name]/[name].js"
|
filename: "[name]/[name].js"
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".tsx", ".js", ".json", ".css"]
|
extensions: [".ts", ".tsx", ".js", ".json"]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({__ENV_WEBEXTENSION: true, __ENV_USERSCRIPT: false}),
|
new RemoveEmptyScriptsPlugin(),
|
||||||
|
new webpack.DefinePlugin({ ...defines, __ENV_WEBEXTENSION: true, __ENV_USERSCRIPT: false}),
|
||||||
new CopyPlugin({ patterns: [
|
new CopyPlugin({ patterns: [
|
||||||
{ from: './src/manifest.json', to: 'manifest.json',
|
{ from: './src/manifest.json', to: 'manifest.json',
|
||||||
transform(content, absoluteFrom) {
|
transform(content, absoluteFrom) {
|
||||||
return allReplace(content.toString(), defines)
|
return allReplace(content.toString(), defines)
|
||||||
},
|
},
|
||||||
@@ -68,14 +104,17 @@ module.exports = [
|
|||||||
]}),
|
]}),
|
||||||
new CopyPlugin({ patterns: [
|
new CopyPlugin({ patterns: [
|
||||||
{ from: './src/icons', to: 'icons/[file]'},
|
{ from: './src/icons', to: 'icons/[file]'},
|
||||||
|
{ from: './src/resources/*.png', to: 'resources/[name][ext]' },
|
||||||
]}),
|
]}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'lib/style.css'
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
// Userscript
|
// Userscript
|
||||||
{
|
{
|
||||||
entry: {
|
entry: {
|
||||||
content: './src/content/content.ts'
|
content: ['./src/content/content.ts', './src/content/GM_fetch/index.js' ]
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||
Reference in New Issue
Block a user